diff --git a/docs/plans/2026-10-21_q195-pane-and-pin.md b/docs/plans/2026-10-21_q195-pane-and-pin.md index 6a17c43dc..3fa483397 100644 --- a/docs/plans/2026-10-21_q195-pane-and-pin.md +++ b/docs/plans/2026-10-21_q195-pane-and-pin.md @@ -95,6 +95,99 @@ Chat tab. It never switches tabs on its own. L6. A chat pane action never touches `docStates`. An unsaved draft stays as it is when the user switches threads, pins or unpins. +## 2a. Any editor in the left pane (DECISIONS Q196: O1, O2, O3) + +Q196 puts three editors on the table: +- O1 extends the row 154 ProseMirror editor. +- O2 runs wordcraft's WebAssembly canvas app in the pane. +- O3 is a mix. + +The pane, the divider and the pin model don't depend on which one is chosen. The pane talks to the left +pane only through the host contract in E1. Pins are keyed by project and path, whatever the file format. + +E1. The host contract: +- `mount(host, {project, path})` and `destroy()`. +- `dirty()`, which the shell asks before any navigation that unmounts the editor. +- `focus()`. +- `selection()`, which returns a locator or null (C2, E5). +- `resized()`, which the shell calls once a drag settles. + +O1 implements the contract directly from `editor/view.mjs`. O2 implements it through an iframe and a +message bridge (E2). + +Measured from wordcraft's web build (`/mnt/storage/src/artcraft/wordcraft`, reference only): +- W1. The canvas covers the whole page (`apps/wordcraft-web/index.html:11`). The app adds listeners to + `document` and `window`: a picture paste handler (`src/web.rs:170-171`) and a `beforeunload` guard + (`src/web.rs:377`, and `guard_unload`). +- W2. No function is exported to JavaScript. A search for `#[wasm_bindgen` in `apps/` and `crates/` finds + nothing. The selection exists only inside the engine and through its MCP document resource + (`crates/mcp/src/server.rs:151`). +- W3. The app keeps preferences in localStorage key `wordcraft.ui` (`apps/wordcraft-web/src/prefs.rs:10`). +- W4. The ribbon shrinks to fit. Groups go compact, then collapse, and the ribbon scrolls if even that + doesn't fit (`crates/ui-egui/src/ribbon_fit.rs:1-13`). The user can also collapse the ribbon + (`ribbon.rs:437`). KeyTips take Alt and F10 (`options.rs:517`). +- Not measured: which element eframe 0.36's keyboard listeners attach to. eframe's source isn't in the + local cargo registry. Louie's evaluation (origin/louie/wordcraft-eval) or the O2 row pins this down. + +E2. O2 runs in an iframe, not in the Console's document. The iframe is served from its own Console +route. +- W1 then stays inside the iframe. Its document-wide paste and key handling can't catch input meant for + the chat pane, and its page-wide canvas fills the iframe instead of the Console. +- The iframe route can carry its own CSP (`wasm-unsafe-eval`) while the Console page's CSP stays as it + is. That is narrower than loosening the editor's CSP, but it is still a CSP change under Q167 D3 and + needs Jason's ruling (J6). +- The iframe has the same origin, so `wordcraft.ui` sits beside the `mosaic-console-*` keys with no + clash (W3). +- Parent and iframe talk by `postMessage` only. The messages are `dirty`, `selection`, `focus-out` and + `resized`. The parent checks `event.origin` and `event.source` on every message. That bridge is new + wordcraft-side code (W2), which the O2 row carries. + +E3. Focus and shortcuts. A shortcut belongs to the pane that has focus. The shell binds no global +editing keys. +- In O1, Ctrl+F and Ctrl+K go to the editor when the editor has focus, as row 154 does now. In the chat + pane they are the browser's. +- In O2, Ctrl+F, Ctrl+K, Ctrl+S, Ctrl+P, Alt and F10 go to wordcraft while the iframe has focus. The + browser delivers keys only to the focused document, so this split comes for free. +- The shell owns two keys. F6 and Shift+F6 cycle focus between the editor, the divider and the chat pane. + Esc in the chat send box returns focus to the editor. +- A canvas app can hold F6. So in O2, wordcraft posts `focus-out` on F6, and the shell moves focus to + the divider. Without that message, a keyboard user can't leave the editor. The O2 row must test this + before anything else. +- A click on either pane focuses that pane in all options. + +E4. Resize. +- O1 reflows with CSS. +- O2 redraws a WebGPU/WebGL surface (Q195 reading, `apps/wordcraft-web/Cargo.toml:3`). + - During a drag the shell sets `pointer-events: none` on the iframe, so the drag doesn't stall when the + pointer crosses the canvas. + - It resizes the iframe once per animation frame and calls `resized()` when the drag ends. + - A keyboard step (L3) is a single resize. + +E5. Selection to the agent. C2 sends `lines` because the C2-06 guard and `renderItems` cite by +`{path, commit, lines, quote}` (M9). +- O1 maps the selection to Markdown source lines (C6). +- O2 asks the iframe for `selection`, and only at send time (C1). + - On a Markdown file opened through wordcraft's Markdown import and export (Q196 reading, README.md:95, + :124), the iframe returns lines of the saved file, and C2 holds unchanged. + - On a .docx file, lines don't exist. A cite needs a new locator, such as a paragraph index range at a + commit, and that changes C2-06 and `render.mjs`. This section doesn't decide that. It goes to Gyro's + format decision and Jason's checklist (J7). +- Until then, a .docx document sends `{project, path, commit}` with no `lines`, and the chip says so. + +E6. Narrow screens with a ribbon. A ribbon needs more width than the O1 toolbar. +- The editor's minimum width is 28rem for O1. For O2, and for O3 when it has a ribbon, it is 44rem. +- The panes stack into tabs (L5) below 60rem for O1, and below 76rem for O2 and O3 with a ribbon. 76rem + is 44rem plus the 18rem chat plus the nav tree and the divider. +- Between the stack point and the sum of the minimums, the shell collapses the chat pane to a 2.5rem + rail with an unread count. It doesn't squeeze the ribbon. +- In the stacked layout the ribbon gets the full width, and wordcraft's own fit and collapse (W4) + handle anything narrower. + +E7. Unsaved work on navigation. Removing an iframe fires no `beforeunload` in it. A hash route change +doesn't fire one either. So before every unmount, in every option, the shell asks the editor for +`dirty()` and asks the user before it navigates away. In O2 that is a `postMessage` round trip with a +1 s timeout. A timeout counts as dirty. + ## 3. Session model and pin S1. What a session is. In the pane, a session is a bus thread (M1). A pin names a thread UUID and nothing @@ -122,8 +215,9 @@ S3. Rules: S4. Two documents pinned to different threads. Document A is pinned to thread T1 and document B to T2. Going from A to B swaps the pane from T1 to T2. Each thread keeps its own draft and cursor (the per-thread -Maps, `chat.js:9`, `:13`). Polling covers the shown thread only. When a reply lands in T1 while T2 is on -screen, it shows as an unread count on the pins list, not as a pane switch. +Maps, `chat.js:9`, `:13`). Polling covers the shown thread only. A reply in T1 while T2 is on screen shows as an unread count on +the pins list, not as a pane switch. The count comes from unread metadata that `thread.pins` returns, not +from polling every pinned thread's messages. S5. A pinned document that was moved or deleted. Pins are keyed by path. The editor never renames (M7), so a move happens in git outside the Console. Pins don't follow git rename detection, because a @@ -173,9 +267,10 @@ chip. C5. Storage and delivery. `thread.send` takes the optional `context`. The broker validates it before the insert: - `project` must be an existing project of the business. -- `path` must pass `documentPath`. -- `commit` must be 40-hex. -- `lines` must be two positive integers with `from ≤ to`. +- `path` must be a path the editor may open for that project: `documentPath`, or the library's mapped + paths on a library business. `documentPath` itself is not widened (gyro-0009). +- `commit` must be 40-hex and reachable from the project's HEAD, and `path` must exist at it. +- `lines` must be two positive integers with `from ≤ to ≤` the file's line count at that commit. - Any extra key is refused with `invalid-context`. The context is stored in a new table, `message_context(message, project, path, commit, line_from, @@ -221,6 +316,12 @@ J5. This design does not depend on the row 5 brief (`docs/plans/2026-09-13_webui #1507), which is waiting on Jason. If Jason's row 5 ruling changes the chat surface, rows P1 and P3 are re-measured. +J6. O2 or O3 with wordcraft: a separate CSP for the iframe route (E2). Q167 D3 bars loosening the +editor's CSP, so this needs Jason's ruling. + +J7. O2 on .docx: a cite locator that isn't line-based (E5). That changes C2-06 and `render.mjs`. It +waits on Gyro's format decision and Jason's ruling. + ## 7. Cross-row findings X1. C2-10b's "Files owned" names `webui/src/public/bus.js`, but thread messages render in @@ -232,7 +333,8 @@ X2. C2-09a's brief cites `broker.mjs:2331-2334` and `:290-291`. At 6c75887b0 the ## 8. Rows -The order is P2 → (P1 ∥ P4) → P3. Merge-chaining applies (Q177): each row builds on the previous row's +The order is P2 → (P1 ∥ P4) → P3. P5 is added only if Jason picks O2, or O3 with wordcraft. P1 builds +the host contract (E1) and the O1 adapter whatever the choice, so P5 plugs into it. Merge-chaining applies (Q177): each row builds on the previous row's candidate and gets its own review. ## P1 Q195 split pane: editor and chat side by side with a keyboard divider @@ -256,7 +358,7 @@ only), a new `pane.js` and `pane.css`, and tests in `packages/webui/tests`. ### What ships -Design sections L1 to L6. Tests: +Design sections L1 to L6, and E1, E3, E4 and E7 for O1. Tests: - The divider's ARIA attributes and keys: arrows, Shift with arrows, Home, End and Enter. - The clamps at 28rem and 18rem. - `mosaic-console-split` is written on release, and a corrupt value gives the default. @@ -394,3 +496,54 @@ Agent edits of the document and replacement proposals (Gyro's section, gated und ### Gate Fenton approves the manifest. The webui suite and the editor round-trip tests are green on Sage's gate. + +## P5 Q195 wordcraft in the left pane: iframe host, its CSP route, and the postMessage bridge + +### Problem + +O2 puts a canvas app in the pane. +- The app listens on `document` and `window` (`apps/wordcraft-web/src/web.rs:170`, `:377`). +- It exports nothing to JavaScript (W2). +- It can hold focus and F6 (E3). + +Without a host and a bridge, the shell can't: +- ask whether there is unsaved work, +- read the selection, +- move keyboard focus out of the editor. + +### Owner and reviewer + +- Owner: a Codex gpt-6.1-sol seat (WebAssembly bridge, CSP route and cross-document messaging). +- Reviewer: an Opus 5.5 seat that is not the author (Fenton). +- After: P1, Jason's rulings J6 and J7, and Gyro's row that brings wordcraft's source into the repo. + +### Files owned + +- `packages/webui/src/serve.mjs`: the iframe route and its CSP only. +- A new `packages/webui/src/public/editor-wordcraft.js` (the host adapter). +- The wordcraft web entry in the repo's copy (bridge messages only), at the path Gyro's row sets. +- Tests in `packages/webui/tests`. + +### What ships + +Design sections E2 to E7 for O2. + +Tests: +- The Console page's CSP is byte-identical before and after. Only the iframe route has + `wasm-unsafe-eval`. +- Messages with the wrong origin or source are ignored. +- F6 inside the iframe moves focus to the divider. +- A `dirty` timeout counts as dirty. +- A paste in the chat pane doesn't reach wordcraft. +- The pane is resized once per animation frame during a drag. +- A Markdown fixture's selection maps to lines, and a .docx fixture sends no `lines`. + +### Out of scope + +- Building wordcraft and its ribbon (Gyro's rows). +- Importing .doc and .qxd files (Q196). +- A new cite locator (J7). + +### Gate + +Fenton approves the manifest. The webui suite is green on Sage's gate, and the CSP test is in that run.