Compare commits

...
+549
View File
@@ -0,0 +1,549 @@
# Q195 pane and pin: split layout, chat session model, document context
Fenton (Opus 5.5), 2026-10-21T15:53:19Z, for Vikunja #448 and DECISIONS Q195. Gyro folds this section
into `docs/plans/2026-10-21_q195-writing-editor.md`. It is measured against origin/refactor 6c75887b0.
Paths are relative to `packages/` unless they start with `docs/`.
This is design only. It covers the split pane, the chat session that stays on screen, the pin, and what
the agent learns about the open document. The thesaurus, document scope and the editor's agent edit
path are Gyro's.
## 1. What exists today (measured)
M1. The Console chat is built from projects and threads. Both are bus UUIDs. Each thread is bound to one
instance, and the bus chooses that instance (`bus/src/broker.mjs:3429-3438`). A user reaches only their
own threads (`broker.mjs:451-457`, REQ-USER-2).
M2. Chat goes through `/api/chat/*` (`webui/src/chat-http.mjs:104-129`), then `webui/src/chat.mjs:20-26`,
then the webui channel plugin. The plugin allows exactly seven verbs: `project.list`, `project.create`,
`thread.list`, `thread.create`, `thread.messages`, `thread.send` and `thread.open`
(`plugins/channel/webui/index.mjs:4-12`). Transport is the human CLI child process
(`webui/src/bus.mjs:187-231`).
M3. Replies arrive by polling every 10 s (`webui/src/public/bus.js:1069`, `:1102-1104`). There is no SSE
or WebSocket.
M4. The chat and the editor both render into `#bus-view`. Every route change destroys the editor
(`bus.js:881`, `:1125`, `:1144`), so the two can't be on screen together. There is no splitter. The only
resize handles are on textareas (`chat.css:16`, `bus.css:48`).
M5. The chat route is parsed from the hash (`chat.js:119`). Chat drafts and read cursors live in
in-memory Maps (`chat.js:9`, `:13`), and the chat stores nothing in the browser. `app.js` keeps two
localStorage keys, `mosaic-console-board` and `mosaic-console-appearance` (`app.js:14`, `:60`).
M6. The chat sends no page or document context. The send body is `{body}` only (`chat.js:232`), and
`chat-http.mjs:112` refuses any other key. `thread.send` accepts only `thread`, `body` and `class`
(`broker.mjs:3480`), and `messages` has no context column (`bus/schema.sql:70-82`).
M7. The editor bundle exports `mount` and `roundTrip` only (`webui/editor/entry.mjs:8`). `mount` returns
`{view, text, setText, focus, destroy}` (`editor/view.mjs:161-208`). It has no selection API. The open
document is `docKey = [project, path]` with the base commit in `docStates` (`bus.js:874-875`, `:908`). A
save is `project.file.save`, which is human-only and refuses a stale base
(`broker.mjs:2411-2429`, `project/src/index.mjs:248-261`). Openable paths are `spec/north-star.md` and
`docs/<name>.md` only (`project/src/paths.mjs:14-15`).
M8. There is no "pin" for chat or documents anywhere in the code. Every `pin` match is unrelated: engine
version, fork account, SSH host key, DNS, persona digest.
M9. Case 2 status:
- C2-08 (persona block) landed in 361ba2387. Creative tools are `read`, `grep`, `find`, `ls` and `web`
(`business/src/vocabulary.mjs:187`).
- C2-06 (cited-reply guard), C2-09a (one thread per creative instance) and C2-10b (cited items in the
Console chat) are briefed, with no code.
- `renderItems` exists in `library/src/render.mjs`, and its cite shape is `{path, commit, lines, quote}`
or `{url, quote}`.
M10. The "sessions" in `git log --grep=session` are mostly SESSIONS.md registry commits. The real session
model is harness sessions kept per thread under `threads/<business>/<thread>/`
(`seat/src/session.mjs:15-26`, `:59-75`, commit 2c16d6b93). The conversation reader
(`conversation/src/reader.mjs:244`, `:400`) is a separate raw-session view. The Console reaches it only
through `/api/conversation(s)`, which is proxied to the board on 7331 (`webui/src/serve.mjs:12`,
`:589-599`).
## 2. Layout
L1. Placement. On `#/docs` and `#/docs/<project>/<path>`, `main` holds three things side by side: the
document area (`#bus-view`), a divider, and a new `aside#chat-pane`. The chat pane is a sibling of
`#bus-view`, not a child of it. A document change re-renders `#bus-view` only, so the chat pane, its
draft and its scroll position survive. Other routes hide the pane. The full `#/chat` route stays as it is.
L2. Embedded chat. `MosaicChat.render` takes a thread id and a container, and does not read or write
`location.hash` in this mode. Today it parses the hash (`chat.js:119`). The pane runs its own 10 s poll
while it is visible, and the poll stops when the tab is hidden or signed out.
L3. Divider. The divider is a focusable element with `role="separator"`, `aria-orientation="vertical"`,
`aria-controls="chat-pane"`, and `aria-valuenow`, `aria-valuemin` and `aria-valuemax` set to the chat
width in percent. This is the WAI-ARIA window splitter pattern.
- Pointer: drag with `setPointerCapture`. A double click resets the split to the default.
- Keyboard: Left and Right arrows move the divider 2%, and Shift with an arrow moves it 10%. Home and End
go to the minimum and maximum chat width. Enter collapses the chat pane, and Enter again restores it.
- Limits: the document area is at least 28rem and the chat pane at least 18rem. The split is clamped
again whenever the window is resized.
- Default: 62% document and 38% chat.
L4. Sizes remembered. One localStorage key, `mosaic-console-split`, holds `{chat: <percent>, collapsed:
<bool>}`. It is written when a drag or key press ends, not on every move. It is per browser, not per
document. That matches the existing `mosaic-console-*` keys (M5). A bad value falls back to the
default.
L5. Narrow screens. Below 60rem the panes stack and a two-tab bar appears ("Document" and "Chat",
`role="tablist"`). This matches the existing ≤959px step in `console.css:34-44`. One pane shows at a
time, and the divider is hidden. Switching tabs keeps the editor draft and the chat draft. The stored
split is kept for the next wide view. A new reply while the Document tab is showing puts a count on the
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
else. It never names a harness session file or a conversation-reader catalogue id. Melanie's chat goes
through `/api/chat` and the bus thread verbs. Her replies render through C2-10b's `renderItems`, never
through `/api/conversation(s)`. So C2-10a (row 220) hiding creative sessions from `catalogue()` and
`open()` does not affect the pane. The pane doesn't use the 7331 board route that row 198 retires.
S2. Two states:
- **Follows you (unpinned).** The pane shows the user's current thread, and it stays through every
document change. The current thread is kept in localStorage `mosaic-console-chat` as `{thread}`. The
key is cleared on sign-out, as the chat's Maps are (`chat.js:23-27`).
- **Pinned.** The pin binds the pair (owner, project, path) to a thread. Opening that document switches
the pane to the pinned thread. Leaving it for a document without a pin brings back the follow-you
thread as it was. The pane header always says which state it is in: "Pinned to docs/ch-01.md" or
"Follows you".
S3. Rules:
- A document has at most one pin per owner.
- A thread may be pinned to several documents, for example a chapter set.
- Pinning while a document is open pins the thread currently shown.
- Unpinning leaves the thread as the follow-you thread.
- When the user switches threads in the pane while a pinned document is open, the pane asks one thing:
"Pin this thread here instead?" It doesn't re-pin silently.
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. 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
heuristic rename match could tie a thread to the wrong chapter. When the pinned path is missing at the
project's HEAD, the pin is stale:
- The pins list shows it as "not found at <short commit>", with two actions, "Re-pin to..." and "Unpin".
- A stale pin is never deleted on its own, and nothing else changes.
- A pin whose thread is gone or no longer the owner's (`thread-not-found`, `thread-not-owned`) shows the
same way, with "Unpin" only.
S6. Where pin state lives. **Decision: the bus**, in a new table:
`thread_pins(business, owner, project, path, thread, at)`, unique on `(business, owner, project, path)`.
| Option | For | Against |
|---|---|---|
| Browser (localStorage) | No schema change, fastest to build | Lost on another machine or browser. Melanie works from her own machine, and the pin would fork per device |
| **Bus** | Follows the user everywhere. `#thread` already checks the owner, so a pin can't expose another user's thread. The bus can validate the path with `documentPath` | A schema migration and three new verbs |
| Project repo (a file) | Lives with the document | Writes chat metadata into her writing repo as commits she didn't make. A shared repo would mix users' pins |
The verbs are `thread.pin {thread, project, path}`, `thread.unpin {project, path}` and `thread.pins
{project?}`. All three are human or tester only, as `thread.send` is (`broker.mjs:3479`). `thread.pin`
runs `#thread` (owner check) and `documentPath`. `thread.pins` reports whether each path exists at HEAD.
The verbs go on the webui channel allowlist (M2) and the `/api/chat` routes. Only the follow-you thread
and the split stay in the browser. Both are per device by nature.
## 4. What the agent learns about the open document, and when
C1. When. Only when the user sends a message. Nothing goes on cursor moves, selection changes, document
opens, or polls. The agent learns what the user chose to send, at the moment it is sent.
C2. What. The send carries an optional `context`:
`{project, path, commit, lines: [from, to]}`.
- `commit` is the base commit of the open document (`docStates`, M7).
- `lines` is the selection as 1-based lines of that commit's text.
- `lines` is present only when the selection is non-empty and the draft is clean. With unsaved edits,
line numbers don't map to any commit. In that case the context carries the path and commit only, and
the chip says "Selection not sent: save first so the assistant can cite it."
C3. No document text goes in the context. The agent reads the lines itself with its `read` tool, which
creative roles already hold (M9). So her prose is not copied into `messages`, and the agent's reply can
cite exactly `{path, commit, lines, quote}`, the shape `renderItems` and the C2-06 guard check (M9).
C4. Consent is visible. Above the send box, a chip shows what will be sent, for example
"docs/ch-01.md · lines 12-14". One click removes it for that message. With no document open, there is no
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 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,
line_to)`, in the same migration as `thread_pins`. `message.read` and `inbox` (`broker.mjs:3050`,
`:3245`) return it as a separate `context` field, never merged into `body`.
C6. Editor support. The editor gains `selection()`, which returns the Markdown source lines covered by
the ProseMirror selection. It uses markdown-it token `map` line ranges and returns null when no clean
mapping exists. The editor package changes nowhere else.
## 5. Case 2 guardrails hold
G1. C2-06, the cited-reply guard. The context is human-to-agent data. The guard checks agent-to-human
messages before the insert (case2-rows.md:385-389), so the pane adds no path around it. The pane renders
replies through C2-10b only. Nothing in this section lets an agent change the document. Any agent edit
or replacement proposal is in Gyro's section, gated on Jason's explicit choice under Q176.
G2. C2-08, the persona block. Context never enters the bundle, the system prompt or the persona file. It
travels as message data in a field of its own. Because `path` must pass `documentPath`, it can't carry a
newline or a `# ` heading, so it can't fake a prompt section.
G3. C2-09a, one thread per creative instance. The pane's "New thread" uses C2-09a's instance picker. A
pin binds a thread, so it binds that thread's instance with it. Bean and Chet threads can be pinned to
different chapters (S4). The pane never picks an instance itself.
G4. M7 and M8 of C2-10a (row 220) are unaffected (S1).
## 6. Dependencies and points for Jason's checklist
J1. A schema change: one migration adding `thread_pins` and `message_context`, plus four verb changes
(`thread.pin`, `thread.unpin`, `thread.pins`, and `context` on `thread.send`). Under AGENTS.md, a new
schema or API needs Jason's approval. The fallback without a schema change keeps pins in localStorage,
loses them across devices, and sends no structured context.
J2. Pin key. The decision is per owner, by path, with no rename following (S5).
J3. Context. The decision is send-time only, with a visible chip and line numbers but no text (C1-C4).
J4. Document scope. The pane works on any path the editor opens. Today that is `docs/*.md` and the spec
only (M7). Melanie's chapters need Gyro's document scope change first.
J5. This design does not depend on the row 5 brief (`docs/plans/2026-09-13_webui-session-chat.md`,
#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
`chat.js:68` as plain `txt(m.body)`. C2-10b needs `chat.js` in its file list, or the cited reply won't
reach either the chat route or the pane. This goes to Sage for row 225.
X2. C2-09a's brief cites `broker.mjs:2331-2334` and `:290-291`. At 6c75887b0 the code is at
`:3429-3438` and `:385-386`. The row has to re-measure before building.
## 8. Rows
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
### Problem
The chat and the editor share `#bus-view`, and each navigation destroys the editor (`bus.js:881`,
`:1125`), so they can't be on screen together. There is no splitter, and `MosaicChat.render` takes its
thread from the hash (`chat.js:119`).
### Owner and reviewer
- Owner: a Haiku 5.5 subagent at high effort (mid-level UI), dispatched by Sage.
- Reviewer: an Opus 5.5 seat that is not the author (Fenton).
- After: C2-10b (row 225), so that replies in the pane render as cited items.
### Files owned
`packages/webui/src/public/index.html`, `bus.js` (the docs routes only), `chat.js` (the embedded mode
only), a new `pane.js` and `pane.css`, and tests in `packages/webui/tests`.
### What ships
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.
- The tab bar appears below 60rem.
- The editor draft and the chat draft both survive a document change and a tab switch.
- The embedded chat never changes `location.hash`.
The webui suite and every `scripts/test-*.sh` pass.
### Out of scope
Pins (P3), context (P4), and the full `#/chat` route.
### Gate
Fenton approves the manifest, and the webui suite is green on Sage's gate.
## P2 Q195 bus pins and message context: thread_pins, message_context, four verbs
### Problem
Nothing stores a pin (M8). `thread.send` accepts no context (`broker.mjs:3480`), and `messages` has no
column for it (`bus/schema.sql:70-82`).
### Owner and reviewer
- Owner: a Codex gpt-6.1-sol seat (higher-level coding: schema, broker, channel).
- Reviewer: an Opus 5.5 seat that is not the author (Fenton).
- After: Jason's yes on J1.
### Files owned
- A new `packages/bus/src/migrations/<next>.sql`, and `packages/bus/schema.sql`.
- `packages/bus/src/broker.mjs`: `thread.pin`, `thread.unpin`, `thread.pins`, `context` on
`thread.send`, and the `context` field in `message.read` and `inbox`.
- `packages/plugins/channel/webui/index.mjs` (the allowlist).
- `packages/webui/src/chat.mjs` and `chat-http.mjs` (the routes and the `context` key).
- Tests in `packages/bus/tests`, `packages/plugins/tests` and `packages/webui/tests`.
### What ships
Design sections S5, S6 and C5. Tests:
- A pin to another user's thread refuses `thread-not-owned`.
- A non-document path refuses.
- Re-pinning replaces the old pin.
- `thread.pins` marks a path missing at HEAD.
- Each bad `context` field refuses `invalid-context`, and the `messages` count is unchanged.
- An agent's `message.read` returns `context` apart from `body`.
- The schema step-count and restore-guard lists are updated (schema.test, T46).
### Out of scope
Any UI (P1, P3) and the editor (P4).
### Gate
Fenton approves the manifest. The bus, plugins and webui suites are green on Sage's gate.
## P3 Q195 pin and follow-you in the chat pane
### Problem
After P1 and P2 the pane can show a thread, but nothing pins it, follows the user, or handles a stale
pin.
### Owner and reviewer
- Owner: a Haiku 5.5 subagent at high effort.
- Reviewer: an Opus 5.5 seat that is not the author (Fenton).
- After: P1 and P2.
### Files owned
`packages/webui/src/public/pane.js`, `chat.js` (the pin controls only), `pane.css`, and tests in
`packages/webui/tests`.
### What ships
Design sections S2 to S5:
- The pin and unpin controls, and the header state line.
- The `mosaic-console-chat` key, cleared on sign-out.
- The swap between pinned and follow-you threads.
- The "Pin this thread here instead?" prompt.
- The pins list with stale and unread marks.
Tests, using fixture documents only:
- Two documents pinned to two threads swap correctly.
- Leaving a pinned document brings back the follow-you thread.
- A stale pin shows its actions and isn't removed on its own.
- A pin with a thread not found shows "Unpin" only.
### Out of scope
Context (P4).
### Gate
Fenton approves the manifest, and the webui suite is green on Sage's gate.
## P4 Q195 send-time document context: editor selection() and the context chip
### Problem
The agent can't tell which document or passage the user means (`chat.js:232` sends `{body}` only), and
the editor has no selection API (`editor/entry.mjs:8`).
### Owner and reviewer
- Owner: a Codex gpt-6.1-sol seat. Mapping ProseMirror positions to Markdown source lines is the hard
part.
- Reviewer: an Opus 5.5 seat that is not the author (Fenton).
- After: P2, and row 214 (C2-11a) on the editor, so the two don't edit the editor package at the same
time.
### Files owned
`packages/webui/editor/view.mjs` and `entry.mjs` (`selection()` only), the rebuilt editor bundle as the
row 154 build makes it, `packages/webui/src/public/pane.js` and `chat.js` (the chip and the `context`
on send), and tests in `packages/webui/tests`.
### What ships
Design sections C1 to C6. Tests:
- `selection()` gives the right lines for a fixture with headings, lists and a wikilink.
- It gives null on a dirty draft.
- The chip appears and can be removed.
- No context is sent with no document open.
- A dirty draft sends the path and commit without `lines`.
- No send carries document text.
### Out of scope
Agent edits of the document and replacement proposals (Gyro's section, gated under Q176).
### 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.