Skip to content

fix: dialogs rendered behind their own scrim; CDP scripts killed the user's Chrome - #244

Merged
HomenShum merged 5 commits into
mainfrom
claude/fresh-user-dialog-fix
Jul 29, 2026
Merged

fix: dialogs rendered behind their own scrim; CDP scripts killed the user's Chrome#244
HomenShum merged 5 commits into
mainfrom
claude/fresh-user-dialog-fix

Conversation

@HomenShum

Copy link
Copy Markdown
Owner

Three defects found while filming product walkthroughs, each invisible to the checks that were supposed to catch it.

1. Every FocusTrapDialog modal rendered behind its own blur scrim

Radix portals dialog overlay and content as siblings. The legacy scrims (.r-modal-backdrop, .r-room-modal-scrim) were written to center a child, and FocusTrapDialog passes unstyled, which drops the Tailwind fixed/translate classes. Every dialog on this path therefore rendered position: static at the end of <body>, under its own z-95 blur — including the create-room dialog that asks the product's one governance question, "How should NodeAgent edits land?"

Tailwind's sr-only is also absent from this bundle, so the fallback DialogTitle rendered the word "Dialog" visibly atop every modal.

DOM text extraction read these dialogs correctly the entire time. Only looking at rendered frames caught it.

2. CDP scripts were force-killing the real browser

  • browser.close() on a connectOverCDP connection closes the actual Chrome, not the socket. Every script ended with it, so each run killed the browser the next run needed — the port looked flaky for hours. Swept the class: 9 pure-CDP scripts drop the call; motion-inventory.mjs connects or falls back to launch(), so it now tracks weLaunchedIt and closes only what it owns.
  • chrome-cdp-up.ps1 ran Stop-Process -Force, giving Chrome no chance to write its session file — the "Chrome didn't shut down correctly" banner and lost tabs. Now graceful CloseMainWindow(), 15s grace, force only as last resort.

3. The verifier asserted videos that no longer exist

yt-verify.mjs hardcoded two ids that are now Private, and matched titles on a phrase the superseded clip and its replacement both carry — so it could pass against the wrong video and report health. Root cause: the roster existed twice, free to disagree. Now one yt-roster.mjs both the verifier and the privatize guard import, with distinguishing expect strings and an import-time throw if an id appears in both lists.

The verifier now checks both directions — 6 published resolve (HTTP 200, titles match), 4 superseded refused (403). The second half is what catches a cleanup that silently did nothing.

Also included

  • boot.ts: the private-route boot shell had no failure exit — a rejected chunk import shimmered "Opening room" forever. Rejection handler, 20s timeout, data-boot-state in the DOM, failed state stops the shimmer and offers Reload.
  • README: the three NodeRoom clips as a roster with ffprobe-verified lengths, the fresh-user GIF inline, and coverage stated honestly — 3 of 6 NodeRoom journeys, 6/21 elements, R5 declined by the owner rather than missed.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
noderoom Ready Ready Preview, Comment Jul 29, 2026 2:58am

Request Review

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

Scaffold Handoff — For Your Coding Agent

Your coding agent (Codex, Claude Code, etc.) should apply the accepted
scaffold proposals below. Do NOT touch any immutable files.

Immutability Check

Mode: advisory

✅ No immutable files were modified in this branch.

Changed Files

  • .qa/HANDOFF-2026-07-28-walkthroughs-motion-proof.md
  • .qa/memory/findings.jsonl
  • AGENTS.md
  • README.md
  • design-dna/observations/obs-greptile-custom-context.yaml
  • design-dna/observations/obs-greptile-pull-requests.yaml
  • design-dna/rules.yaml
  • docs/eval/OFFICIAL_BENCHMARK_READINESS.md
  • docs/eval/OFFICIAL_BENCHMARK_TASK_COVERAGE.md
  • docs/eval/OPENROUTER_CONVEX_BENCHMARK.md
  • docs/eval/agent-improvement-loop.md
  • docs/eval/agent-improvement-loop.svg
  • docs/eval/agent-improvement-loop/20260729T025757Z.json
  • docs/eval/agent-improvement-loop/latest.json
  • docs/eval/agent-workspace-sandbox-smoke.json
  • docs/eval/algorithm-artifact-smoke.json
  • docs/eval/bankertoolbench-official-contract.json
  • docs/eval/docker-sandbox-probe.json
  • docs/eval/eval-runs.jsonl
  • docs/eval/halo-convex-context-telemetry.json
  • docs/eval/halo-self-improvement-smoke.json
  • docs/eval/halo-variant-selection.json
  • docs/eval/official-benchmark-readiness.json
  • docs/eval/official-benchmark-task-coverage.json
  • docs/eval/openrouter-convex-benchmark.json
  • docs/eval/professional-catalog-proofs.json
  • docs/eval/professional-proof-ledger.json
  • docs/eval/spreadsheetbench-chart-visual-probe.json
  • docs/eval/traces/credit/20260729T025805866Z-f0e3dd96_dirty.736bb70de9febbd5/cascade-healthy.json
  • docs/eval/traces/credit/20260729T025805866Z-f0e3dd96_dirty.736bb70de9febbd5/delta-incomplete.json
  • docs/eval/traces/credit/20260729T025805866Z-f0e3dd96_dirty.736bb70de9febbd5/mapping-correct.json
  • docs/eval/traces/credit/20260729T025805866Z-f0e3dd96_dirty.736bb70de9febbd5/mapping-misbind.json
  • docs/eval/traces/credit/20260729T025805866Z-f0e3dd96_dirty.736bb70de9febbd5/summit-stressed.json
  • docs/eval/traces/ladder/20260729T025805391Z-f0e3dd96_dirty.c6e7f9753a38a934/ladder_L1_read_scripted.json
  • docs/eval/traces/ladder/20260729T025805391Z-f0e3dd96_dirty.c6e7f9753a38a934/ladder_L2_edit_scripted.json
  • docs/eval/traces/ladder/20260729T025805391Z-f0e3dd96_dirty.c6e7f9753a38a934/ladder_L3_conflict_scripted.json
  • docs/eval/traces/ladder/20260729T025805391Z-f0e3dd96_dirty.c6e7f9753a38a934/ladder_L4_blocked_scripted.json
  • docs/eval/traces/ladder/20260729T025805391Z-f0e3dd96_dirty.c6e7f9753a38a934/ladder_L5_large_range_scripted.json
  • docs/eval/traces/ladder/20260729T025805391Z-f0e3dd96_dirty.c6e7f9753a38a934/ladder_L6_long_horizon_scripted.json
  • docs/eval/traces/ladder/20260729T025805391Z-f0e3dd96_dirty.c6e7f9753a38a934/ladder_L7_resume_scripted.json
  • docs/walkthroughs/fresh-user-landing.gif
  • index.html
  • scripts/assemble-gif.mjs
  • scripts/capture-ui.mjs
  • scripts/chrome-cdp-up.ps1
  • scripts/motion-inventory.mjs
  • scripts/playwright-peer.mjs
  • scripts/record-ui.mjs
  • scripts/shot-boot-failstate.mjs
  • scripts/trust-surface-audit.mjs
  • scripts/trust-surface-core.mjs
  • scripts/trust-surface-live.mjs
  • scripts/trust-surface-selftest.mjs
  • scripts/verify-boot-failstate.mjs
  • scripts/yt-cdp-attach.mjs
  • scripts/yt-cdp.mjs
  • scripts/yt-edit-probe.mjs
  • scripts/yt-privatize.mjs
  • scripts/yt-probe.mjs
  • scripts/yt-retitle.mjs
  • scripts/yt-roster.mjs
  • scripts/yt-save-probe.mjs
  • scripts/yt-upload.mjs
  • scripts/yt-verify.mjs
  • skills/motion-proof/SKILL.md
  • skills/motion-proof/fixtures/00-honest-control.html
  • skills/motion-proof/fixtures/01-exists-but-never-mounts.html
  • skills/motion-proof/fixtures/02-offscreen-decoy.html
  • skills/motion-proof/fixtures/03-clock-only-diff.html
  • skills/motion-proof/fixtures/04-trust-surface-toward-approval.html
  • skills/motion-proof/fixtures/05-reduced-motion-different-design.html
  • skills/motion-proof/fixtures/06-knockout-jumps-to-end.html
  • skills/motion-proof/fixtures/07-video-shows-absent-motion.html
  • skills/motion-proof/motion-probe.mjs
  • skills/motion-proof/profiles/genjutsu.yaml
  • src/app/styles.css
  • src/landing/boot.ts

Needs Adversarial Review — Do NOT Apply Yet

These proposals passed the reject check but have not been approved by
an adversarial reviewer. A human or frozen LLM judge must approve them first.

  • scaf-001 (AGENTS.md): Add explicit instruction for step spreadsheetbench-runner-fixture: Step spreadsheetbench-runner-fixture failed — scaffold may need explicit instruction or evidence assertion.
  • scaf-002 (AGENTS.md): Add explicit instruction for step convex-boundaries: Step convex-boundaries failed — scaffold may need explicit instruction or evidence assertion.

Safety Boundary

Agent may improve the scaffold.
Agent may NOT weaken the proof gate.

Immutable files (never modify):

  • scripts/proofloop.mjs
  • scripts/agent-improvement-loop.ts
  • tests/harnessChangeEval.test.ts
  • .github/workflows/
  • src/eval/evalTrustPolicy.ts
  • src/eval/architectureBudget.ts
  • evals/evalStore.ts

Scaffold files (safe to modify):

  • AGENTS.md
  • CLAUDE.md
  • proofloop/scenarios/*.yaml
  • proofloop/rubrics/*.yaml
  • proofloop/subagents/*.md
  • proofloop/adapters/*.js
  • .proofloop/memory.jsonl
  • src/nodeagent/models/prompts/systemPrompt.ts

HomenShum and others added 4 commits July 28, 2026 19:52
Radix portals dialog overlay and content as SIBLINGS. The legacy scrims
(.r-modal-backdrop grid place-items:center, .r-room-modal-scrim) were written
to center a CHILD, and FocusTrapDialog passes `unstyled`, which drops the
Tailwind fixed/translate classes from DialogContent. Every dialog on this path
therefore rendered position:static at the end of <body>, BEHIND its z-95 blur
overlay — including the create-room dialog that asks the product's one
governance question ('How should NodeAgent edits land?'). Tailwind's sr-only
is also absent from this bundle, so the fallback DialogTitle rendered the word
'Dialog' visibly at the top of every modal.

Found by reviewing walkthrough footage frame by frame: DOM text extraction read
the dialog fine the whole time. Fix: center [data-slot=dialog-content] itself
above its scrim, and define .sr-only.

Also included, same session:
- boot.ts: the private-route boot shell had no failure exit — a rejected or
  hung chunk import shimmered 'Opening room' forever. Rejection handler, 20s
  timeout backstop, data-boot-state in the DOM, failed state stops the shimmer
  and offers Reload. 13/13 scenario checks incl. reduced-motion.
- scripts/: trust-surface gate (core + selftest probed in BOTH directions +
  live runner), motion inventory (painted-only, reduced-motion sweep), CDP
  YouTube upload/retitle with ffprobe-derived titles, capture/verify tooling.
- design-dna/: 28 cited facts + 6 rules from a live Greptile inspection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ries

chrome-cdp-up.ps1 is idempotent and encodes the three flags that each cost a
debugging cycle: --user-data-dir must be explicit and UNQUOTED (a quoted path
with a space fails the launch silently, and Chrome ignores the debugging port
without it), and --disable-extensions is required because a real profile loads
~13 extension service workers as CDP targets that stall connectOverCDP.

yt-upload gains the three narrated videos and derives every title duration
from ffprobe rather than a typed constant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two independent defects, both of which made the debugging port look flaky when
the scripts were the cause:

1. browser.close() on a connectOverCDP connection CLOSES THE REAL BROWSER.
   Playwright reads it as 'close the browser', not 'drop the socket'. Every
   script ended with it, so each run killed the Chrome the next run depended
   on — which is why the port 'kept dying' and got relaunched all session
   instead of diagnosed. Swept the class, not the instance: 9 pure-CDP scripts
   drop the call; motion-inventory.mjs connects OR falls back to launch(), so
   it now tracks weLaunchedIt and closes only a browser it owns.

2. chrome-cdp-up.ps1 ran Stop-Process -Force on every chrome process. A force
   kill gives Chrome no chance to write its session file, which is what
   produced the 'Chrome didn't shut down correctly / Restore pages?' banner and
   lost the user's open tabs. Now CloseMainWindow() (equivalent to clicking X),
   15s grace, force only for a window that refuses, and --restore-last-session
   on relaunch.

Also: yt-privatize.mjs — set superseded uploads to Private over CDP. Private,
never delete: it hides a video from everyone including link-holders and is
reversible. Guards by video ID against a keeper allowlist because two
superseded clips share a title prefix with a keeper, so any title match would
eventually hide the wrong one; the guard was tested in the failing direction
first.

README: the three NodeRoom clips as a roster with ffprobe-verified lengths, the
fresh-user GIF inline, and an honest coverage note — 3 of 6 NodeRoom journeys,
6/21 elements, R5 declined by the owner rather than missed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
yt-verify.mjs hardcoded YUpSMEkkK4Q and q1CL1hCO_0Q — both superseded and set
Private earlier today. It also matched titles on 'review every agent change',
a phrase the superseded clip and its REPLACEMENT both carry, so it could have
passed against the wrong video and reported health. yt-save-probe.mjs
navigated to the same dead id.

Root cause is not the stale ids, it is that the roster existed twice: once in
the verifier and once in the privatize guard, free to disagree. Introduced
scripts/yt-roster.mjs as the single source both import, with  strings
that DISTINGUISH rather than merely match (both narrated titles contain 'the
full walkthrough, narrated', so each carries its product), and an import-time
throw if an id ever appears in both published and superseded.

Rewrote yt-verify.mjs around two changes:
  - no browser. It used connectOverCDP, so a verifier could only run when
    Chrome was up with a debugging port, and before today's fix could take the
    user's browser down with it. oembed answers 'is this public, and what is
    its title' over plain HTTP with no session.
  - it checks BOTH directions. Confirming the good ids resolve proves nothing
    about the ones that should be gone; the superseded half is what would
    catch a privatize that silently did nothing.

Verified: 6 published PASS (HTTP 200, titles match), 4 superseded PASS (HTTP
403). Guard re-tested in the failing direction against the imported list.

Reported by a subagent reviewing files outside its assigned scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
skills/motion-proof previously lived only in ~/.claude, which is not version
controlled — so the one artifact this session built from scratch could not be
handed to anyone, backed up, or reviewed. It now sits beside skills/liveflow,
skills/probe-first and the rest, which was already the convention.

It ships the primary instrument that was missing: an audit found the skill
INVERTED in practice — SKILL.md correctly names Element.getAnimations() primary
and the video judge advisory, but getAnimations() had zero executable callers
while six Gemini video-judge scripts ran. motion-probe.mjs is that instrument,
with seven adversarial fixtures and an honest control. The control must pass and
every deception must be caught; the first run found two false positives in the
probe itself (opacity is not a paint test, because enter animations start at
opacity 0; and transform:none equals the identity matrix).

Also resolves an unresolved stash conflict in .qa/memory/findings.jsonl. Both
sides were distinct valid records in an append-only log, so the union was kept —
29 records, all parsing — rather than picking a winner.
@HomenShum
HomenShum merged commit 7c097e8 into main Jul 29, 2026
11 of 13 checks passed
@HomenShum
HomenShum deleted the claude/fresh-user-dialog-fix branch July 29, 2026 03:01
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.

1 participant