plans: Q195 pane and pin, editor-independent host contract for O1, O2 and O3 (Q196), context validation per gyro-0009 (#448)

This commit is contained in:
jason.woltje committed 2026-10-21 10:57:17 -05:00
1 parent ac34d97862
commit 7caf8008b5
1 file changed
+160 -7
+160 -7
View File
@@ -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 L6. A chat pane action never touches `docStates`. An unsaved draft stays as it is when the user switches
threads, pins or unpins. 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 ## 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 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. 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 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 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
screen, it shows as an unread count on the pins list, not as a pane switch. 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 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 (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 C5. Storage and delivery. `thread.send` takes the optional `context`. The broker validates it before the
insert: insert:
- `project` must be an existing project of the business. - `project` must be an existing project of the business.
- `path` must pass `documentPath`. - `path` must be a path the editor may open for that project: `documentPath`, or the library's mapped
- `commit` must be 40-hex. paths on a library business. `documentPath` itself is not widened (gyro-0009).
- `lines` must be two positive integers with `from ≤ to`. - `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`. - Any extra key is refused with `invalid-context`.
The context is stored in a new table, `message_context(message, project, path, commit, line_from, 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 #1507), which is waiting on Jason. If Jason's row 5 ruling changes the chat surface, rows P1 and P3 are
re-measured. 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 ## 7. Cross-row findings
X1. C2-10b's "Files owned" names `webui/src/public/bus.js`, but thread messages render in 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 ## 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. candidate and gets its own review.
## P1 Q195 split pane: editor and chat side by side with a keyboard divider ## 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 ### 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 divider's ARIA attributes and keys: arrows, Shift with arrows, Home, End and Enter.
- The clamps at 28rem and 18rem. - The clamps at 28rem and 18rem.
- `mosaic-console-split` is written on release, and a corrupt value gives the default. - `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 ### Gate
Fenton approves the manifest. The webui suite and the editor round-trip tests are green on Sage's 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.