Skip to content

Chat: own-words edit proposals; My Words text prototype folds into Chat - #698

Merged
kcarnold merged 13 commits into
mainfrom
chat-doc-tools
Oct 31, 2026
Merged

kcarnold merged 13 commits into
mainfrom
chat-doc-tools

Conversation

@kcarnold

@kcarnold kcarnold commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Closes #583. Chat becomes a coach that can also propose edits to the document, under the My Words rule: it may only place the writer's own words, and every edit needs the writer's approval. The My Words text prototype (Walkthrough/Propose) is removed; Voice stays as a prototype on the shared code.

Design write-up: docs/chat-document-tools.md.

What changed

Shared code. The My Words tool logic moved to frontend/src/docTools/ (ops, the word-bank check, apply and report, plus the new edit note, preview and undo). EditorAPI.applyEdit is now optional; canEdit(editor) decides whether edit tools are offered. Google Docs omits it (#590); Word and the standalone editor have it.

Chat context (supersedes #249). The transcript is the model's own ModelMessage[], tool calls included. Each writer turn carries a <document_context> block:

  • the whole document, numbered, on the first turn;
  • after that, a paragraph diff of what the writer changed since the model last looked;
  • the text around the cursor.

The writer never sees this block. view re-reads the document on demand.

Proposals. str_replace, insert and move are gated in toolApproval. Each must dry-run cleanly against the current document and pass the word-bank check; otherwise it's denied with a reason the model can act on. A proposal that passes gets an approval card (summary, a trimmed preview of the change, Accept or Decline). Undo can then revert any applied edit, in any order, and refuses if the text has changed since.

Word bank:

  • the live document, plus every paragraph the model has seen this conversation (so deleted words can be reinstated);
  • what the writer typed;
  • the brief.

Runs of content words may be stitched together from pieces that overlap by two or more words. as was dropped from the glue words because it can mean "because".

Logging. New chatLog.edit events for the proposal lifecycle (schema version 6). Content goes in proposal and offending, which are document-level in the consent gate.

Departures from #583's plan

  • The app writes every answer to a proposal itself: the approval response plus the tool result. On Accept it applies the edit first, then calls the model. The SDK resolves approvals only when they're the last message, and it skips approvals that already have a result. Writing both parts ourselves keeps the history valid whatever follows: a new writer message ("set aside") or a failed model call. I checked this against the installed ai@7 source; the tests run the real SDK.
  • Edits are on wherever applyEdit exists, including Word, rather than standalone only. Word may still pick the wrong text to edit (Send hosts ranges, not needles: one text-matching implementation across editors #589).
  • Overlap rule instead of whole-run matching.
  • Seen paragraphs count as the writer's words, so deleted text can be reinstated.
  • Undo reverts any applied edit, not only the latest.

Testing

  • npm run typecheck, npx vitest run (270 passing), eslint.
  • New tests cover:
    • the end-to-end proposal loop through the SDK with a scripted model: word-bank denial, a no-match denial, a pending card, Accept applying exactly once without the model's edit leaking into the next edit note, and a set-aside proposal leaving a valid history;
    • the edit-note diff;
    • preview trimming;
    • undo planning, including relocation and refusal;
    • the overlap rule and its denial messages.
  • Tested live in the standalone editor, including a word-bank denial and a stitched edit.

Follow-ups: #699.

🤖 Generated with Claude Code

kcarnold and others added 10 commits October 31, 2026 16:23
It was yucky that we had to have these custom wrappers, but we already litigated that and ended up with vercel/ai#17974
…e corpus, if overlap is at least two words.

Also remove `as` as a stitch word, since it reads LLM-y.
Walkthrough/Propose, their interaction/ seam, and the scripted demo page
are gone: Chat's own-words edits replace Propose. My Words is now the voice
prototype alone, on the shared docTools/. The mock editor moves to
__fixtures__ for the voice tests. Docs that described the removed code
point at docs/chat-document-tools.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@kcarnold kcarnold mentioned this pull request Oct 31, 2026
10 tasks
- Edit tools' `execute` now throws instead of applying. The app writes every
  proposal's result itself, so the SDK never runs it; if that invariant ever
  breaks, the proposal fails visibly rather than editing from a second path.
  Drop the unreachable 'applying' status: an approval without a result now
  reads as 'failed', never 'pending' (which would offer Accept again).
- A new proposal no longer selects its target in the document, which could
  replace the selection with the writer's next keystroke and made the next
  turn's context report a selection the writer never made. The card gets a
  "Show in document" link instead, using the doctext jump's trimming so long
  paragraphs still resolve in Word.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017KwGFirZNMaz3WBQCU4Xor
Undoing a `move` puts the paragraph back with a splice that removes nothing,
so it had no text to relocate by and went back at its recorded index. After
a later edit added or removed a paragraph above it, the paragraph silently
landed in the wrong place. Record the neighbors on such splices
(`prevParagraph`/`nextParagraph`, the fields #589 plans for hosts to enforce)
and re-anchor between them: exactly one match, or refuse.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017KwGFirZNMaz3WBQCU4Xor
The previous commit matched a put-back's whole neighbor paragraphs exactly,
so any edit anywhere in either neighbor blocked the undo, and blank
neighbors made it ambiguous. Undo data is long-lived, so anchor it the way
#589's TextAnchor does: ~32 characters on each side of the gap, counted
across paragraph breaks (blank paragraphs included literally), matched as
far as a window cut short by the document's edge goes.

The anchors live on an undo-only `UndoSplice` (captured by `captureUndo`),
not on `ParagraphSplice`, whose neighbor fields #589 reserves for exact host
preconditions; hosts get plain splices. A comment on `contextAt` records the
skip-blank-lines alternative in case refusals cluster around blank lines.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017KwGFirZNMaz3WBQCU4Xor
@kcarnold
kcarnold merged commit 5902c14 into main Oct 31, 2026
7 checks passed
@kcarnold
kcarnold deleted the chat-doc-tools branch October 31, 2026 22:34
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.

Chat: move view and highlight tools from my-words

2 participants