Repository navigation
Show a web page's still frame under a toast it arrives beneath, instead of a gray pane - #358
Merged
Merged
Conversation
…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>
6 tasks done
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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 screeninshell.log). The poster controller does not ask again during one cover, so the pane stayed parked with no frame.The owner's
shell.logshows this exact order: the refusal a few milliseconds before theowner=mainattach line, then the view parked until the toast went.How
WebPageSurface.tsx. The postercapturewaits one microtask before sendingweb-poster, so the same publish'sweb-geometry-reportleaves 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.WebPanetest 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 typecheckclean; client suite green (5,265). The fullpnpm testrun 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.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'sposter N base64 bytesline. Red on main (13/15, with the shell's refusal), 15/15 with the fix (offscreen window).web-pane-middle-click-and-modal-poster19/19 andweb-pane-comes-back-after-hide8/8, so menus and whole-window modals still poster as before.Manual test steps
🤖 Generated with Claude Code