Skip to content

Show a web page's still frame under a toast it arrives beneath, instead of a gray pane - #358

Merged
benfriebe merged 1 commit into
mainfrom
fix/web-poster-under-toast
Oct 6, 2026
Merged

benfriebe merged 1 commit into
mainfrom
fix/web-poster-under-toast

Conversation

@benfriebe

Copy link
Copy Markdown
Owner

Deleting a worktree workspace lands the window on its neighbour. Clicking a workspace with a web pane while the "Removed worktree" toast was still up showed that pane as an empty gray hole until the toast was dismissed or expired.

What you see

  • A web pane that comes on screen under a toast now shows its page. It wears a still frame of the page, as it already did when a toast appeared over a pane that was on screen.
  • Dismissing the toast brings the live page back, as before.
  • The same applies to any surface a pane arrives under, including the sticky "Kept on disk" toast, which used to leave the pane gray until it was dismissed.

Cause

A covered web pane parks its native view and wears a still frame of the page (the poster). A pane that arrives on screen already covered did two things in one publish: asked the shell for a frame (web-poster), then sent the placement that puts its view on screen. The ask left first, so the shell refused a frame of a view it had not placed (poster refused - the view is not on screen in shell.log). The poster controller does not ask again during one cover, so the pane stayed parked with no frame.

The owner's shell.log shows this exact order: the refusal a few milliseconds before the owner=main attach line, then the view parked until the toast went.

How

  • WebPageSurface.tsx. The poster capture waits one microtask before sending web-poster, so the same publish's web-geometry-report leaves first. Both travel the same socket in order, and the shell places a view synchronously when the report arrives (embed.apply → tab.setEmbedded(true)), so the frame is taken of the view on screen.
  • Tests. A new WebPane test pins the order for a pane that mounts under an open surface (placed, then one ask, nothing parked yet). Four existing tests asserted the ask went out synchronously and now flush one microtask first.

Testing

  • pnpm typecheck clean; client suite green (5,265). The full pnpm test run had one other failure, daemon/tests/compat/graft.test.ts "syncs a later worktree edit into the parent while watching" (31 s timeout). It passes on its own and this change does not touch the daemon.
  • New scripts/scenarios/web-pane-poster-under-worktree-toast.mjs. It opens a web pane, creates a real worktree workspace and a neighbour, and deletes the worktree workspace from the sidebar. It then clicks the web pane's workspace while the toast is up and checks the poster <img> and the shell's poster N base64 bytes line. Red on main (13/15, with the shell's refusal), 15/15 with the fix (offscreen window).
  • web-pane-middle-click-and-modal-poster 19/19 and web-pane-comes-back-after-hide 8/8, so menus and whole-window modals still poster as before.

Manual test steps

  1. Open a web pane on the right of a workspace (it needs to sit under the bottom-right toast).
  2. Create a worktree workspace below it in the sidebar, plus one more workspace below that.
  3. Go to the worktree workspace and delete it from the sidebar, leaving its worktree ticked. The window lands on the workspace below, and the "Removed worktree" toast shows.
  4. While the toast is up, click the workspace with the web pane: the page shows under the toast instead of a gray pane.
  5. Click the toast: the live page is back and responds to clicks.

🤖 Generated with Claude Code

…ad of a gray pane

Deleting a worktree workspace lands the window on its neighbour, and
clicking a workspace with a web pane while the "Removed worktree" toast
was still up showed that pane as an empty gray hole until the toast
went.

A covered pane wears a still frame of its page (the poster). A pane
that arrives on screen already covered asked the shell for that frame
in the same publish that placed its view, but before the placement was
sent. The shell refused a frame of a view it had not placed ("the view
is not on screen"), and the pane parked with no frame for the rest of
the cover. The capture now waits one microtask, so the placement leaves
first on the same socket and the shell, which places a view
synchronously, photographs the view on screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@benfriebe
benfriebe merged commit 4957beb into main Oct 6, 2026
@benfriebe
benfriebe deleted the fix/web-poster-under-toast branch October 6, 2026 23:59
benfriebe added a commit that referenced this pull request Oct 16, 2026
…hem (#419)

## Problem
A toast in the bottom right (a workspace deleted, "Removed worktree",
Zen Mode) took a web pane away while it was up. You could not scroll the
page, and the keyboard left it.

The toast stack was drawn in the client's document, which sits under a
web pane's native page view. So it registered its box
(`useOverlayPresence`) and every page it overlapped was parked: swapped
for a still poster, which a wheel cannot scroll. The park also handed
the keyboard to the client (`releaseKeyboardIfHeld`) and nothing gave it
back when the toast went.

## Change
The Electron shell now draws the toast stack itself, in a see-through
`WebContentsView` (the **corner layer**) placed over the stack's box,
above every page view. The page under a toast stays live, scrolls, and
keeps the keyboard.

- **Client** (`client/src/chrome/corner-layer.ts`, `ToastStack.tsx`):
the stack moved out of `App.tsx` into `ToastStack.tsx`. Inside the shell
(`?shellWindow=`) the page exposes `globalThis.__kelpiCorner`. While the
shell is attached, the page still lays the stack out (it is the
measurement) but at opacity 0, marks it `data-drawn-by="shell"`, and
does not register its box, so nothing parks. If the shell stops taking
frames for 1.5 s (`UNTAKEN_FRAME_MS`) the page detaches it and draws the
stack itself again.
- **Overlay page** (`client/overlay.html`,
`client/src/overlay/CornerOverlay.tsx`): a new Vite entry that renders
the same `ToastCards` on a transparent document and reports clicks back.
- **Shell** (`shell/src/corner-layer.ts`, `corner-layer-view.ts`): a
pure controller (probe, frame loop, click loop, placement with a 12 px
margin, up to 3 overlay restarts, then it gives the stack back to the
page) and its Electron view (transparent background, own partition, no
background throttling). Both directions are `executeJavaScript`
long-polls, as `quit-prompt.ts` already does: no preload and no daemon
protocol change.
- **Keyboard:** the layer samples who holds the keyboard as the pointer
enters it, and when a click focuses the layer it hands the keyboard
straight back (`webHost.keyboardOwner()` / `restoreKeyboard()`, newly
exposed).
- **Stacking:** adding a page view puts it on top, so the web host now
calls `onViewAttached` and the layer re-raises itself.
- **Fallback:** wherever the layer is not drawing (a browser, the phone,
an older shell, a layer that failed) the stack is drawn in the page and
parks the panes it covers, poster and all, as before.

`docs/shell-ui.md` §4.2 gains a "Toast-over-a-web-page requirement"
paragraph.

## Reviewer notes
- Plugin notifications (`BundledPrompts` `Notifications`) share the
corner but were **not** moved: they are still drawn in the page and
still park. The toast layer draws above them where the two overlap.
- The scenario `web-pane-poster-under-worktree-toast` (#358) is renamed
to `web-pane-live-under-toast` and rewritten: it used to assert the
poster under the toast, and now asserts the page stays live, the card is
drawn by the layer, and a click on the layer's card dismisses the toast.
It keeps #358's "arrive at the pane while the toast is up" round.
- Every call into the page or the overlay is guarded
(`cornerCallScript`). A call sent while the client window reloads runs
in the new document, sometimes before its bridge is installed; it
answers a sentinel instead of throwing an uncaught error in the page,
and the controller hands the stack back to the page until the next load.
The first battery run caught this (`plugin-browser-features`,
`plugin-terminal-features`); fixed in 8419e92.
- Keyboard hand-back and on-screen compositing can't be checked from the
scenario lane (lane windows are keyless). The owner tested them by hand
in a dev instance.

## Test plan
- [x] Typecheck clean (client and shell).
- [x] Unit tests: root 11,550 passed, shell 1,084 passed, 43 of them new
(`client/src/chrome/corner-layer.test.tsx`,
`client/src/overlay/CornerOverlay.test.tsx`,
`shell/src/corner-layer.test.ts`).
- [x] `node scripts/scenario.mjs --window offscreen
web-pane-live-under-toast`: 25/25.
- [x] Also green alone: `web-pane-middle-click-and-modal-poster`,
`plugin-backend-prompts`, `cmd-click-codex-links`,
`terminal-copy-paste-chords`, `recover-interface-keeps-web-panes`,
`web-pane-comes-back-after-hide`, `workspace-agent-delete`.
`plugin-interaction-presenters` fails only in its cleanup step, the
pre-existing #396.
- [x] By hand in a dev instance: a web pane on a long page, a toast
raised over it. The page scrolls and keeps typing under the toast, and
after a click on the toast the keyboard is still in the page.
- [x] `node scripts/verify.mjs --since origin/main` full battery on
8419e92: typecheck, root tests (11,550), shell tests (1,084), bundles,
full audit and packaged smoke (75/75) passed. Scenario lane 62/68; every
red is also red on `main` or a known issue (details in the comment
below).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01DMT4Nhd3hzhXa2EEaHi4u7

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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