Skip to content

feat(sdk): show a queued marker on data-lavish-question scopes - #219

Open
stone16 wants to merge 5 commits into
kunchenguid:mainfrom
stone16:codex/queued-question-feedback
Open

feat(sdk): show a queued marker on data-lavish-question scopes#219
stone16 wants to merge 5 commits into
kunchenguid:mainfrom
stone16:codex/queued-question-feedback

Conversation

@stone16

@stone16 stone16 commented Aug 2, 2026

Copy link
Copy Markdown

What Changed

  • The artifact SDK now stamps prompts queued from inside a data-lavish-question wrapper with an internal _lavishQuestionKey, and the chrome mirrors those keys back into the sandboxed iframe over a new lavish:setQueuedQuestionKeys message — on every render and again on each iframe load, since the chrome owns queue state and a live reload resets the SDK. The field joins _lavishQueueKey in stripInternalPromptFields, so it never reaches /api/:key/prompts. Queueing with an explicit empty queueKey opts out of both replacement and the marker.
  • Each queued question scope gets one persistent role="status" overlay (✓ Queued) that is repositioned rather than rebuilt, so the queued state is announced once instead of on every pass. Repositioning is rAF-coalesced and driven by capture-phase scroll (nested scrollers don't bubble), resize, a ResizeObserver, and a MutationObserver that catches attribute-driven reflows; observers start only while a key is queued. When the scope is clipped fully out of view the marker fades instead of unmounting.
  • Added intersectClipRects to src/artifact-sdk.js (serialized into the injected SDK by src/server.js) to clamp the position: fixed marker against every scroll/overflow ancestor's padding box plus the viewport, so a question inside a scrollable panel no longer rings the content after it; non-finite clip edges are ignored rather than collapsing the box. README documents the marker and its opt-out, AGENTS.md records the _lavishQuestionKey protocol, and tests cover the SDK geometry, chrome queue sync/clear/restore, and SDK-serialization guards.

Risk Assessment

✅ Low: The follow-up commit cleanly resolves both round-1 findings exactly as instructed (stable per-scope role=status markers with style-only repositioning, plus capture-phase scroll, attribute/characterData mutation, and ResizeObserver coverage), introduces no observer feedback loops or leaks thanks to shadow-boundary isolation and queue-gated lifecycle, and leaves the queue protocol, chrome sync, and tests intact.

Testing

No baseline commands had been run for this round, so I started from the targeted automated tests: test/artifact-sdk.test.js and test/chrome-client-queue.test.js (104 pass, covering the five new intersectClipRects cases) plus the two name-filtered queued-question guards in test/server.test.js that pin the clamp into the serialized SDK. Because this is a purely visual overlay change, I then ran it end to end in a real browser: I served a purpose-built artifact through lavish-axi and clicked through the actual Lavish chrome, with a data-lavish-question form clipped by an overflow:auto panel and a deliberately marked block immediately below it. The marker now stops at the panel's padding edge (bottom 492) instead of the question's real bottom (550), so it no longer paints 21px of green ring over the block starting at 529; clamping is two-sided, applies equally to overflow:hidden, and respects the padding box rather than the border box, while a question with no clipping ancestor still rings its full box. Fading fully out of view kept the same marker node and a single role="status" live region, and the queue → remove → send lifecycle cleared markers correctly with the internal question key stripped from the agent payload. No console errors, no findings. I deliberately did not run the opt-in LAVISH_AXI_BROWSER_E2E=1 suites — they exercise the layout audit and whiteboard rather than this code path, and my own real-browser run covers it directly. Teardown left the worktree clean.

  • Evidence: Marker clamped to the scrollable panel's edge — ring stops at the panel, the "must NOT be ringed" block below is untouched (local file: /var/folders/14/wd6c42m15v1c7q5ph2jb9bh40000gn/T/no-mistakes-evidence/01KZ0FQTZ9FPQC7TVKJS95Z71E/32-marker-clamped-to-panel-edge.png)
  • Evidence: Both questions queued — unclipped question rings its full box, clipped one stops at the panel edge (local file: /var/folders/14/wd6c42m15v1c7q5ph2jb9bh40000gn/T/no-mistakes-evidence/01KZ0FQTZ9FPQC7TVKJS95Z71E/33-both-questions-queued.png)
  • Evidence: An overflow:hidden ancestor clamps identically (applied via live reload) (local file: /var/folders/14/wd6c42m15v1c7q5ph2jb9bh40000gn/T/no-mistakes-evidence/01KZ0FQTZ9FPQC7TVKJS95Z71E/38-overflow-hidden-ancestor-also-clamps.png)
Evidence: Measured marker geometry vs. the pre-fix viewport-only clamp, marker-identity and lifecycle table

panel padding box 264/492 · question's own box 380–550 · pre-fix viewport-only clamp would end at 550 · actual marker 380–492 · #after-panel occupies 529–581 (previously overlapped by 21px, now untouched). Panel scrolled: marker top pinned to 264, not the question's real top 260/206. Question fully below the fold: node kept as m1 with opacity "0" (no re-announcement). Unclipped question: 72–242, exactly its own border box.

# Queued-question marker geometry — real browser, target commit c65697f

Measured by reading live `getBoundingClientRect()` values inside the running artifact
(`clip-demo.html` served by `lavish-axi` at `127.0.0.1:64034`, Chrome, chrome page 1280x900).
All values are CSS px in the artifact iframe's viewport.

`viewportOnlyClampBottom` is what the pre-fix formula `Math.min(rect.bottom, window.innerHeight)`
would have produced — computed in the page from the same live rects, for comparison only.

## The clipped question (`data-lavish-question="audit-logs"`, inside `#panel { overflow: auto }`)

| state | panel padding box (top/bottom) | question's own box | viewport-only clamp would end at | actual marker | opacity |
|---|---|---|---|---|---|
| panel scrollTop 0 | 264 / 492 | 380 – 550 | **550** | 380 – **492** | `""` |
| panel scrollTop 120 | 264 / 492 | 260 – 430 | 430 | **264** – 430 | `""` |
| panel scrolled to end | 264 / 492 | 206 – 376 | 376 | **264** – 376 | `""` |
| panel back to scrollTop 0 | 264 / 492 | 380 – 550 | 550 | 380 – **492** | `""` |
| viewport shrunk to 274px tall (question entirely below the fold) | 264 / 492 | 380 – 550 | — | node kept, geometry frozen | **`"0"`** |
| viewport restored to 844px tall | 264 / 492 | 380 – 550 | 550 | 380 – **492** | `""` |

`#after-panel` ("Content after the panel — must NOT be ringed") occupies **529 – 581**.
The old viewport-only clamp ended at 550, i.e. it painted 21px of green ring over that block.
The clamped marker ends at 492 and never reaches it.

Clamping is two-sided: scrolling the panel down pins the marker's **top** to the panel's padding
top (264) instead of the question's real top (260 / 206).

### Same panel switched to `overflow: hidden` (via live reload, `artifact_revision=2`)

| panel padding box | question's own box | viewport-only clamp would end at | actual marker |
|---|---|---|---|
| 264 / 492 | 380 – 550 | 550 | 380 – **492** |

`overflow: hidden` clamps identically to `overflow: auto` — the walk keys off
`overflowX/Y !== "visible"`, so `hidden` / `clip` / `auto` / `scroll` all bound the marker.
The panel has a 6px border; the marker stops at 492 (the **padding** box), not 498 (the border
box), so the ring never paints over the panel's own border.

## The unclipped question (`data-lavish-question="rollout-plan"`, no clipping ancestor)

marker `m2` = 72 – 242, left 140 / right 780 — exactly the form's own border box. No shrinkage,
so the fix does not regress the ordinary case.

## Marker identity / announcement

Each marker node was stamped with a probe id the first time it was seen.
The clipped question's marker stayed **`m1`** across every row above — including fading to
`opacity: "0"` and back — so the node is repositioned, never rebuilt, and the
`role="status" aria-label="Answer queued"` live region announces once.

Chrome's accessibility tree during the two-queued state showed exactly two live regions:

`` `
uid=2_0 status "Answer queued" atomic live="polite" relevant="additions text"
  uid=2_1 StaticText "✓ Queued"
uid=3_0 status "Answer queued" atomic live="polite" relevant="additions text"
  uid=3_1 StaticText "✓ Queued"
`` `

## Lifecycle

| action | markers after |
|---|---|
| queue answer for `audit-logs` | 1 (`m1`) |
| queue answer for `rollout-plan` | 2 (`m1`, `m2`) |
| remove the `audit-logs` pill in the Conversation panel | 1 (`m2` only) |
| **Send to Agent** | 0 |
- Evidence: Queued state before annotation (baseline render of the demo artifact) (local file: /var/folders/14/wd6c42m15v1c7q5ph2jb9bh40000gn/T/no-mistakes-evidence/01KZ0FQTZ9FPQC7TVKJS95Z71E/30-before-queueing.png) - Evidence: Queueing an answer inside the scroll panel shows the ✓ Queued marker and the pill in the Conversation panel (local file: /var/folders/14/wd6c42m15v1c7q5ph2jb9bh40000gn/T/no-mistakes-evidence/01KZ0FQTZ9FPQC7TVKJS95Z71E/31-queued-inside-scroll-panel.png) - Evidence: Removing a queued prompt clears only that question's marker (local file: /var/folders/14/wd6c42m15v1c7q5ph2jb9bh40000gn/T/no-mistakes-evidence/01KZ0FQTZ9FPQC7TVKJS95Z71E/34-removing-prompt-clears-its-marker.png) - Evidence: Send to Agent clears all queued-question markers (local file: /var/folders/14/wd6c42m15v1c7q5ph2jb9bh40000gn/T/no-mistakes-evidence/01KZ0FQTZ9FPQC7TVKJS95Z71E/36-send-to-agent-clears-markers.png)
Evidence: Agent poll payload — internal _lavishQuestionKey stripped from the delivered prompt

prompts[1]{uid,prompt,selector,tag,text}: "2","Rollout plan: Canary then full",form#q-plan,form,Which rollout plan? (not inside any scroller) Canary then full Big bang Queue answer

session:
  file: /private/var/folders/14/wd6c42m15v1c7q5ph2jb9bh40000gn/T/no-mistakes-evidence/01KZ0FQTZ9FPQC7TVKJS95Z71E/clip-demo.html
  status: feedback
dom_snapshot: "uid=3 body \"Release checklist Which rollout plan? (not inside any scroller) Canary then full\"\n  uid=4 main \"Release checklist Which rollout plan? (not inside any scroller) Canary then full\"\n    uid=5 h1 \"Release checklist\"\n    uid=2 form \"Which rollout plan? (not inside any scroller) Canary then full Big bang Queue an\"\n      uid=6 h2 \"Which rollout plan? (not inside any scroller)\"\n      uid=7 label \"Canary then full\"\n        uid=8 input\n      uid=9 label \"Big bang\"\n        uid=10 input\n      uid=11 button \"Queue answer\"\n    uid=12 div \"Scrollable panel. The question below is clipped by this panel's edge. Ship audit\"\n      uid=13 p \"Scrollable panel. The question below is clipped by this panel's edge.\"\n      uid=1 form \"Ship audit logs in v1? Yes No, defer to v2 Queue answer\"\n        uid=14 h2 \"Ship audit logs in v1?\"\n        uid=15 label \"Yes\"\n          uid=16 input\n        uid=17 label \"No, defer to v2\"\n          uid=18 input\n        uid=19 button \"Queue answer\"\n      uid=20 p \"Trailing content so the panel actually scrolls.\"\n    uid=21 p \"Content after the panel — must NOT be ringed.\"\n  uid=22 script \"// Per README's Feedback controls guidance: option clicks only update local stat\"\n  uid=23 script"
prompts[1]{uid,prompt,selector,tag,text}:
  "2","Rollout plan: Canary then full",form#q-plan,form,Which rollout plan? (not inside any scroller) Canary then full Big bang Queue answer
next_step: "Apply the requested changes to /private/var/folders/14/wd6c42m15v1c7q5ph2jb9bh40000gn/T/no-mistakes-evidence/01KZ0FQTZ9FPQC7TVKJS95Z71E/clip-demo.html. Do not respond to the user just yet. Now you must run `lavish-axi poll /private/var/folders/14/wd6c42m15v1c7q5ph2jb9bh40000gn/T/no-mistakes-evidence/01KZ0FQTZ9FPQC7TVKJS95Z71E/clip-demo.html --agent-reply \"<message for the user>\"` without --timeout-ms unless the user ended the session. The poll waits silently until the user sends more feedback or ends the session - never kill it. Keep the poll in the foreground by default and let it return the feedback directly to the agent. A background poll is allowed only through a harness-native tracked background-job facility whose completion result is guaranteed to resume or notify the same agent. Never use `nohup`, shell `&`, `disown`, redirected fire-and-forget processes, or a detached terminal without an explicit verified callback merely to keep polling alive. If the harness has no completion-aware background facility, use the foreground poll or first wire a verified wake callback into the surrounding supervisor. Do not tell the user the artifact is being monitored until that wake path is live. If the poll gets killed or times out anyway, just re-run it - queued feedback is never lost."
Evidence: Demo artifact used for the end-to-end run (question clipped by a scrollable panel + geometry probe)
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Release checklist</title>
    <style>
      :root {
        color-scheme: light;
      }
      body {
        margin: 0;
        padding: 24px;
        font:
          15px/1.5 ui-sans-serif,
          system-ui,
          sans-serif;
        background: #eef2f6;
        color: #15202b;
      }
      main {
        max-width: 640px;
        margin: 0 auto;
        display: grid;
        gap: 16px;
      }
      h1 {
        font-size: 21px;
        margin: 0;
      }
      .card {
        background: #fff;
        border: 1px solid #d3dbe4;
        border-radius: 12px;
        padding: 16px 18px;
      }
      .card h2 {
        font-size: 15px;
        margin: 0 0 8px;
      }
      label {
        display: block;
        padding: 3px 0;
      }
      button {
        margin-top: 10px;
        padding: 8px 14px;
        border: 0;
        border-radius: 8px;
        background: #15202b;
        color: #fff;
        font: inherit;
        font-weight: 600;
        cursor: pointer;
      }

      /* The scrollable panel. Its question is TALLER than the space left inside it, so the
         browser clips the question at the panel's padding edge. A viewport-only clamp would
         let the queued marker paint straight over #after-panel below. */
      #panel {
        height: 200px;
        overflow: hidden;
        background: #fff;
        border: 6px solid #94a3b8;
        border-radius: 12px;
        padding: 14px 16px;
      }
      #panel .filler {
        height: 90px;
        margin: 0 0 12px;
        color: #61707f;
      }
      #after-panel {
        background: #fde68a;
        border: 1px dashed #b45309;
        border-radius: 12px;
        padding: 14px 18px;
        color: #7c2d12;
        font-weight: 600;
      }
    </style>
  </head>
  <body>
    <main>
      <h1>Release checklist</h1>

      <form class="card" data-lavish-question="rollout-plan" id="q-plan">
        <h2>Which rollout plan? (not inside any scroller)</h2>
        <label><input type="radio" name="plan" value="Canary then full" /> Canary then full</label>
        <label><input type="radio" name="plan" value="Big bang" /> Big bang</label>
        <button type="button" id="queue-plan">Queue answer</button>
      </form>

      <div id="panel">
        <p class="filler">Scrollable panel. The question below is clipped by this panel's edge.</p>
        <form class="card" data-lavish-question="audit-logs" id="q-audit">
          <h2>Ship audit logs in v1?</h2>
          <label><input type="radio" name="audit" value="Yes" /> Yes</label>
          <label><input type="radio" name="audit" value="No, defer to v2" /> No, defer to v2</label>
          <button type="button" id="queue-audit">Queue answer</button>
        </form>
        <p class="filler">Trailing content so the panel actually scrolls.</p>
      </div>

      <p id="after-panel">Content after the panel &mdash; must NOT be ringed.</p>
    </main>

    <script>
      // Per README's Feedback controls guidance: option clicks only update local state, one final
      // answer is queued from a per-question submit button.
      function wire(buttonId, formId, inputName, label) {
        document.getElementById(buttonId).addEventListener("click", () => {
          const form = document.getElementById(formId);
          const picked = form.querySelector('input[name="' + inputName + '"]:checked');
          if (!picked) return;
          window.lavish.queuePrompt(label + ": " + picked.value, { element: form });
        });
      }
      wire("queue-plan", "q-plan", "plan", "Rollout plan");
      wire("queue-audit", "q-audit", "audit", "Audit logs");

      // ---- evidence probe (not part of the feature) --------------------------------------------
      // The artifact iframe is sandboxed without allow-same-origin, so the driver cannot reach in.
      // This relays user-equivalent actions and reads back real rendered geometry.
      let stamped = 0;
      const round = (n) => Math.round(n);
      function boxOf(el) {
        const r = el.getBoundingClientRect();
        return { top: round(r.top), left: round(r.left), right: round(r.right), bottom: round(r.bottom) };
      }
      function paddingBoxOf(el) {
        const r = el.getBoundingClientRect();
        const s = getComputedStyle(el);
        return {
          top: round(r.top + parseFloat(s.borderTopWidth)),
          left: round(r.left + parseFloat(s.borderLeftWidth)),
          right: round(r.right - parseFloat(s.borderRightWidth)),
          bottom: round(r.bottom - parseFloat(s.borderBottomWidth)),
        };
      }
      function readState() {
        const shadow = document.querySelector(".lavish-annotation-root")?.shadowRoot;
        const markers = [...(shadow?.querySelectorAll(".lavish-queued-question") || [])];
        for (const m of markers) if (!m.dataset.probeId) m.dataset.probeId = "m" + ++stamped;
        const audit = document.getElementById("q-audit");
        const panelPad = paddingBoxOf(document.getElementById("panel"));
        const auditBox = boxOf(audit);
        return {
          viewport: { w: window.innerWidth, h: window.innerHeight },
          panelPaddingBox: panelPad,
          auditQuestionBox: auditBox,
          afterPanelBox: boxOf(document.getElementById("after-panel")),
          // What the old viewport-only clamp WOULD have produced for the clipped question.
          viewportOnlyClampBottom: Math.min(auditBox.bottom, window.innerHeight),
          markers: markers.map((m) => ({
            id: m.dataset.probeId,
            role: m.getAttribute("role"),
            ariaLabel: m.getAttribute("aria-label"),
            text: m.textContent.trim(),
            opacity: m.style.opacity,
            ...boxOf(m),
          })),
        };
      }
      window.addEventListener("message", (event) => {
        const msg = event.data || {};
        if (!String(msg.type || "").startsWith("demo:")) return;
        if (msg.type === "demo:pick") document.querySelector(msg.selector).click();
        if (msg.type === "demo:queue") document.getElementById(msg.id).click();
        if (msg.type === "demo:scrollPanel") document.getElementById("panel").scrollTop = msg.top;
        // Report after two frames so the SDK's requestAnimationFrame reposition has run.
        requestAnimationFrame(() =>
          requestAnimationFrame(() => window.top.postMessage({ type: "demo:state", ...readState() }, "*")),
        );
      });
    </script>
  </body>
</html>
- Outcome: 🔧 1 issue found → auto-fixed ✅ across 2 runs (29m58s)

Pipeline

Updates from git push no-mistakes

⏭️ **intent** - skipped

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ⚠️ src/artifact-sdk.js:1865 - Queued-question markers are position:fixed overlays repositioned only on window scroll (non-capture listener), window resize, and childList mutations. Three common cases move the question wrapper without firing any of these: (1) scrolling an inner overflow container (scroll events from nested scrollables never reach a non-capture window listener), (2) attribute-driven reflows such as class toggles for accordions/tabs (the MutationObserver observes childList+subtree only, not attributes), and (3) late image/font loads. In each case the persistent marker visually detaches from its wrapper and overlays unrelated content until the next window scroll/resize/mutation. Fix mechanically: add a capture-phase scroll listener ({ capture: true, passive: true }) dedicated to scheduleQueuedQuestionRender, and/or observe marked scopes with ResizeObserver (already declared as a global in this file).
  • ⚠️ src/artifact-sdk.js:1687 - renderQueuedQuestionState removes and re-creates marker elements carrying role="status" (implicit aria-live polite) on every render pass, and passes are scheduled per animation frame during scrolling plus on every DOM mutation. Screen readers that announce newly inserted status regions (the common toast pattern) will re-announce "Answer queued" repeatedly while the user scrolls or the artifact mutates. Reuse persistent marker nodes and update only their position styles, or keep role="status"/aria-label on one stable node per question so the queued state is announced once when it changes.
  • ℹ️ src/artifact-sdk.js:702 - queuePrompt deliberately skips _lavishQuestionKey when the caller passes an explicit empty queueKey (queuePrompt(text, { queueKey: "" })) inside a data-lavish-question wrapper, so such answers queue with no visible Queued marker. This diverges from the new README sentence stating question wrappers "show a visible Queued state while their answer is waiting to be sent" (unconditionally). The Object.hasOwn carve-out looks intentional (empty queueKey is the opt-out of replacement semantics), but the marker suppression is a separate product decision worth confirming: either document the opt-out edge in README or drop the explicitQueueKey condition so any queued answer from a question wrapper shows the marker.

🔧 Fix: keep queued-question markers stable and attached through reflows
1 info still open:

  • ℹ️ src/artifact-sdk.js:1752 - While any question answer is queued, the dedicated MutationObserver watches the entire document with attributes+characterData+childList+subtree, so every style/class/text mutation anywhere coalesces into a rAF pass running document.querySelectorAll("[data-lavish-question]") plus getBoundingClientRect reads. This is a deliberate, commented tradeoff and is fully gated (observers start only when keys are queued, stop when empty; artifacts without questions pay nothing), with no feedback loop because all render-pass writes stay inside the shadow root that subtree observation cannot pierce. Acceptable as shipped; only worth revisiting if an animation-heavy artifact ever shows jank while an answer is queued (e.g. by caching the scope list between structural mutations).
🔧 **Test** - 1 issue found → auto-fixed ✅
  • ⚠️ src/artifact-sdk.js:1699 - The queued-question marker is clamped only to the viewport, not to ancestor clipping/scroll containers, so a question inside a scrollable panel paints its green ring over unrelated content outside that panel. positionQueuedQuestionMarker computes the box from scope.getBoundingClientRect() and clamps with Math.min(rect.right, window.innerWidth) / Math.min(rect.bottom, window.innerHeight) only. Reproduced: a data-lavish-question form inside an overflow:auto panel is clipped at the panel edge, but its ring extends ~200px below the panel and frames the paragraph after it (evidence 10-marker-overflows-nested-scroll-container.png). Unlike the transient .lavish-text-highlight, which shares this fixed-overlay limitation but is cleared when the annotation card closes, this marker persists for the whole review, so the bleed is visible the entire time. Whether ancestor clipping is in scope for this change is the author's call.
  • node --test test/chrome-client-queue.test.js (72 tests, includes the 3 new queued-question sync/clear/restore tests)
  • node --test --test-name-pattern "artifact SDK (renders queued|keeps queued-question)" test/server.test.js
  • Manual E2E: started a real server via node bin/lavish-axi.js &lt;artifact&gt; --no-open with an isolated LAVISH_AXI_STATE_DIR and ephemeral port, opened the session URL in Chrome via chrome-devtools
  • Manual: clicked a radio + "Queue answer" in the artifact iframe -> verified the green ✓ Queued ring and label appear on that question only (screenshot 02)
  • Manual: re-queued a different option -> verified exactly one marker and one replaced pill remain (screenshot 03)
  • Manual: edited the artifact file to trigger live reload -> verified markers restored from the persisted queue (screenshot 04)
  • Manual: scrolled the nested #scroller container -> measured question top 559->429 and marker top 559->429, marker node id unchanged (screenshot 05)
  • Manual: expanded &lt;details&gt; (attribute-only reflow, no childList/resize/scroll event) -> measured plan 155->245 and audit 429->518 with markers matching and node ids unchanged (screenshot 06)
  • Manual: clicked "Remove queued prompt" on one pill -> verified only that question's marker cleared, the other node survived unchanged (screenshot 07)
  • Manual: resized viewport 960->520 so a queued question left the viewport -> verified opacity: &#34;0&#34; with the node retained, and restored on resize back
  • Manual: take_snapshot of the accessibility tree -> verified two status &#34;Answer queued&#34; regions with atomic / live=&#34;polite&#34;
  • Manual: node bin/lavish-axi.js poll &lt;artifact&gt; --timeout-ms 45000 while clicking "Send to Agent" -> verified the agent receives {uid,prompt,selector,tag,text} with _lavishQuestionKey stripped, and that markers cleared on send (screenshot 09)

🔧 Fix: clamp queued-question markers to clipping ancestors
✅ Re-checked - no issues remain.

  • node --test test/artifact-sdk.test.js test/chrome-client-queue.test.js — 104 tests pass, including the five new intersectClipRects geometry tests (scrollable-ancestor clamp, no-clipping-ancestor passthrough, nested innermost tightening, empty intersection, non-finite clip edges)
  • node --test --test-name-pattern &#34;queued-question&#34; test/server.test.js — both SDK-serialization guards pass, including the one that fails if the viewport-only Math.min(rect.right, window.innerWidth) clamp comes back
  • Manual end-to-end: served a purpose-built artifact (clip-demo.html, a data-lavish-question form clipped by an overflow:auto panel, with a marked "must NOT be ringed" block right after it) via node bin/lavish-axi.js &lt;file&gt; --no-open on an ephemeral port + isolated LAVISH_AXI_STATE_DIR, then drove it in Chrome through chrome-devtools with real mouse clicks on the radio and Queue answer button
  • Measured live getBoundingClientRect() geometry inside the running artifact across panel scrollTop 0 / 120 / end / back, comparing the actual marker box against the pre-fix Math.min(rect.bottom, window.innerHeight) value computed from the same rects
  • Verified the marker node is repositioned and never rebuilt: probe-stamped id stayed m1 through fading to opacity:&#34;0&#34; (viewport shrunk to 274px so the question fell fully below the fold) and back, and Chrome's a11y tree showed exactly one status &#34;Answer queued&#34; atomic live=&#34;polite&#34; region per queued question
  • Verified overflow: hidden clamps identically by editing the demo's panel CSS and letting live reload apply it (artifact_revision=2), then re-queueing the answer
  • Verified the queued-marker lifecycle end to end: queue → remove pill in the Conversation panel → Send to Agent, checking marker count after each
  • node bin/lavish-axi.js poll &lt;file&gt; --timeout-ms 8000 — confirmed the delivered prompt carries only {uid,prompt,selector,tag,text}, i.e. the internal _lavishQuestionKey is stripped before reaching the agent
  • mcp__chrome-devtools__list_console_messages (errors + warnings, preserved across navigations) — none
  • git status --porcelain after teardown — clean; temp state dir and detached server removed
🔧 **Document** - 1 issue found → auto-fixed ✅
  • ℹ️ README.md:164 - README states that wrappers marked with data-lavish-question show a Queued state, but src/artifact-sdk.js:731-733 only sets _lavishQuestionKey when the attribute has a non-empty value AND the caller either omits queueKey or passes a non-empty one. So queuePrompt(..., { queueKey: &#34;&#34; }) (or data-lavish-question=&#34;&#34;) silently produces no marker. This reads as a deliberate opt-out tied to the dedupe key, so I did not widen the README sentence — confirm whether that opt-out is intended user-facing contract worth one clause, or an implementation detail README should keep omitting.

🔧 Fix: document queued-question marker opt-out in README
✅ Re-checked - no issues remain.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

style.textContent = `:host{all:initial;position:fixed;z-index:2147483647;left:0;top:0;color-scheme:dark;--ink-900:#0f1115;--ink-800:#11141a;--ink-700:#171a21;--ink-600:#1c212b;--steel-700:#2a2f3a;--steel-600:#303745;--steel-500:#3c4557;--steel-400:#8c96aa;--steel-300:#aeb6c6;--steel-200:#b9c0cf;--steel-100:#d8deea;--cream-50:#fffbf3;--cream-100:#f7f3ea;--cream-200:#e8e1cf;--brass-500:#f4c95d;--brass-400:#ffd877;--brass-ink:#17130a;--bg:var(--ink-900);--bg-panel:var(--ink-800);--bg-elevated:var(--ink-600);--fg:var(--cream-100);--fg-faint:var(--steel-300);--border:var(--steel-600);--accent:#f4c95d;--accent-hover:#ffd877;--font-sans:Geist,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;--font-mono:"Geist Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--radius-md:10px;--radius-xl:14px;--shadow-floating:0 20px 70px rgba(0,0,0,.35);font-family:var(--font-sans)}*{box-sizing:border-box}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.lavish-text-highlight{position:fixed;pointer-events:none;background:rgba(244,201,93,.28);border-radius:2px;box-shadow:0 0 0 1px rgba(244,201,93,.45)}.lavish-queued-question{position:fixed;pointer-events:none;border:2px solid #22c55e;border-radius:10px;box-shadow:0 0 0 3px rgba(34,197,94,.18)}.lavish-queued-question-label{position:absolute;right:6px;top:6px;padding:4px 8px;border-radius:999px;background:#166534;color:#f0fdf4;box-shadow:0 2px 8px rgba(0,0,0,.24);font:700 12px/1.2 var(--font-sans);letter-spacing:.01em;white-space:nowrap}.lavish-annotation-card{position:fixed;width:min(320px,calc(100vw - 24px));padding:12px;border-radius:var(--radius-xl);background:var(--bg-panel);color:var(--fg);border:1px solid var(--accent);box-shadow:var(--shadow-floating);font:14px/1.4 var(--font-sans)}.lavish-heading{font-weight:700;margin-bottom:6px}.lavish-annotation-card textarea{width:100%;min-height:86px;resize:vertical;border-radius:var(--radius-md);border:1px solid var(--border);background:var(--bg);color:var(--fg);padding:9px;font:inherit;font-family:var(--font-sans)}.lavish-annotation-card textarea::placeholder{color:var(--fg-faint)}.lavish-annotation-card .lavish-hint{margin-top:6px;font-size:11px;color:var(--fg-faint)}.lavish-annotation-card .lavish-row{display:flex;gap:8px;justify-content:flex-end;margin-top:8px}.lavish-annotation-card button{border:0;border-radius:var(--radius-md);padding:8px 10px;font-family:var(--font-sans);font-size:13px;font-weight:700;cursor:pointer}.lavish-annotation-card button:active{opacity:.85}.lavish-annotation-card .lavish-send{background:var(--accent);color:var(--brass-ink)}.lavish-annotation-card .lavish-send:hover{background:var(--accent-hover)}.lavish-annotation-card .lavish-cancel{background:var(--steel-700);color:var(--fg)}.lavish-reveal-marker{position:fixed;pointer-events:none;border:2px solid var(--accent);border-radius:4px;box-shadow:0 0 0 4px rgba(244,201,93,.22);animation:lavish-reveal-pulse 2.4s var(--ease,ease-out) forwards}@keyframes lavish-reveal-pulse{0%{opacity:0}12%{opacity:1}70%{opacity:1}100%{opacity:0}}`;

P2 Badge Clip the queued label to the visible marker bounds

When a queued question is only partly visible inside a scroll container—for example, only a few pixels remain above the panel's bottom edge—intersectClipRects correctly shrinks the marker, but this absolutely positioned label still overflows that shrunken box because .lavish-queued-question does not clip its contents. The green “Queued” pill can therefore render over content outside the panel even though the new geometry logic is intended to keep the overlay within the question's clipping chain; clip or hide/reposition the label when the visible intersection is too small.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

This is a hold. The PR is CONFLICTING with main, and the change itself is default chrome/SDK UX: every data-lavish-question scope would gain a persistent "✓ Queued" overlay plus a new lavish:setQueuedQuestionKeys protocol without the artifact author opting into that chrome. That is a product decision (VISION: an artifact's design is chosen, never defaulted into; nothing interrupts the human), not a corrective or opt-in fix, so it is not auto-mergeable even if the conflict were resolved. Codex also noted the queued pill can still overflow a clipped marker. Required CI (no-mistakes, build-and-test) was green on this head; I am not rebasing or flagging while CONFLICTING.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants