amal66 brings Mike's assistant into Microsoft Word

This fork turns Word into a practical front end for Mike, while leaving lawyers in control of each edit.

workflowsecurity

The standalone Word add-in brings the assistant's chat, projects and workflows into a task pane alongside the document.

  • Tracked edits: generated rewrites become real Word changes, ready for normal accept or reject review.
  • Document actions: proofread, anonymise, improve writing and draft clauses without leaving the editor.
  • Context-aware chat: the assistant can consider the open document when answering or proposing revisions.
  • Workflows and projects: existing repeatable processes and matter context are available from the pane.

The document is treated as reference material rather than instructions, with protections against text in a file trying to steer the assistant. The team also tested the pane in Word Online, including its edit and review flows.

So what Legal teams that live in Word should care because this makes AI-assisted drafting and review feel closer to an ordinary document workflow, rather than another tab to manage.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

22 commits from amal66/mike, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
3f490983 fix(chat): honor the Word add-in's documentContext on POST /chat Amalanand Muthukumaran 2026-07-25 ↗ GitHub
commit body
POST /chat silently dropped the add-in's `documentContext` field (it
parsed only messages/chat_id/project_id/model/ask_inputs_response), so
the Chat tab's "Use document as context" toggle and the Workflows tab
ran WITHOUT the document - no error, just answers that never saw the
text. The add-in README's claim that the chat route fences the document
into the system prompt was false against this tree.

Port the fork's minimal backend support:

- parseOptionalDocumentContext: validate the optional string field
  (400 on non-strings), trim, and cap at 200k chars so an oversized
  body can't blow the context window.
- buildWordDocumentContextPrompt: inject the document into the system
  prompt via buildMessages's existing (previously unused on this route)
  systemPromptExtra parameter. The body is user-controlled text and a
  prompt-injection vector, so it is nonce-fenced via spotlight() -
  unpredictable per-request nonce on BOTH tags, smuggled fence tokens
  HTML-encoded, echoed nonces redacted - preceded by an instruction
  that it is reference content, not instructions.
- Focused vitest coverage for the parsing, the fence, and the
  end-to-end injection through buildMessages.

This makes the PR no longer purely additive to word-addin/: it touches
backend/src/routes/chat.ts and backend/src/lib/chat/contextBuilders.ts,
because without the backend half the add-in's headline feature does not
work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
b8bd5b0c feat: Word add-in - chat, tracked-changes rewrites, workflows and projects in Word Amalanand Muthukumaran 2026-07-25 ↗ GitHub
commit body
Port of the Office.js Word add-in from amal66/mike@feat/word-addin-pr. The
add-in itself (word-addin/) is copied verbatim; in the fork it consumes the
shared @mike/api-client, @mike/core, and @mike/shared packages from the
monorepo's packages/ directory via tsconfig paths + webpack aliases. Upstream
has no packages/ directory, so the specific files it imports are vendored as
mechanical copies under word-addin/src/vendor/{api-client,core,shared}/ and
the aliases retargeted there - no source file changes.

Features: chat task pane with SSE streaming, document-anchored tracked-changes
rewrites, workflow runs, project browsing/upload, API-key onboarding banner,
Supabase auth, hermetic Playwright e2e suite with an in-page Office.js shim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CEguyEgXa9JjCciXCcVemC
686f9cee feat(word-addin): apply Proofread/Anonymise results to existing text as tracked redlines Amalanand Muthukumaran 2026-08-02 ↗ GitHub
commit body
WHY THIS MATTERS
Until now the add-in could only make tracked changes in two narrow ways:
replacing a range the user had manually selected (Improve Writing), and
inserting brand-new paragraphs (Draft Clause / chat). Proofread and
Anonymise produced read-only prose: the user had to hunt down each issue
and retype the fix by hand. For a legal drafting tool, "the AI can see
the problem but cannot redline it" is the core missing feature - lawyers
work in tracked changes precisely so every machine-proposed edit stays
reviewable and rejectable.

WHAT IS A TRACKED REDLINE
Word's change tracking (the Review tab) records each edit as a pair of
marks - deleted text struck through, inserted text underlined - that the
reviewing lawyer can accept or reject individually. Programmatically,
Office.js exposes this via document.changeTrackingMode: any edit made
while the mode is TrackAll is recorded as if a human typed it.

HOW IT WORKS
1. The Proofread and Anonymise prompts now mandate a machine-readable
   block format (shared constant REDLINE_FORMAT):
       ORIGINAL: <verbatim snippet from the document>
       REPLACEMENT: <corrected / anonymised text>
       REASON: <one short sentence>
   The format is deliberately dual-purpose: it reads naturally while it
   streams into the result box, and it parses exactly once the stream
   completes (lib/redline.ts, tolerant of list numbering and bold).
2. A new useWordDoc.applyTrackedEdits() locates each ORIGINAL with
   Word's search API (case-sensitive, every occurrence) and replaces it
   via insertText(..., Replace) while changeTrackingMode is TrackAll,
   restoring the user's previous tracking mode afterwards.
3. Honesty over guessing: originals that are unsearchable (multi-
   paragraph, or over Word's ~255-char search limit) or no longer
   present (the user edited the document mid-stream) are counted and
   reported - "Applied 2 of 3 ... 1 skipped" - never fuzzily matched,
   because a wrong redline in a contract is worse than no redline.
4. Edits are parsed only after a clean stream finish (never mid-stream,
   which could apply a half-received replacement) and duplicate
   ORIGINALs are deduped, since the second application would fail its
   search after the first replacement already rewrote the text.

TESTING
Four new hermetic Playwright specs against the in-page Office.js shim:
tracked application of parsed corrections (asserting the exact recorded
Word calls and TrackAll mode), skip-and-report for stale text, no apply
button on "No issues found.", and tracked PII redaction. Suite: 61/61.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
903184e2 feat(word-addin): chat-proposed tracked redlines + opt-in e2e video recording Amalanand Muthukumaran 2026-08-03 ↗ GitHub
commit body
WHY THIS MATTERS
The Actions tab could already redline the document, but chat - the
surface lawyers actually live in - could only append text below the
cursor. Competing Word integrations (Legora et al.) center on the
"discuss, then accept redlines" loop: you ask for changes in chat and
the answer lands as tracked changes you accept or reject. This commit
closes that gap by reusing the existing redline engine end-to-end.

HOW IT WORKS
1. A "Suggest tracked edits" switch in the chat composer. While on, the
   document text is always sent as context (redlines are meaningless
   without it - the model must copy ORIGINAL snippets verbatim), and
   the outgoing user turn invisibly carries the shared REDLINE_FORMAT
   contract (now exported from lib/redline.ts, single-sourced with the
   Proofread/Anonymise prompts). The transcript shows only what the
   user typed.
2. Every COMPLETED assistant message is parsed with parseRedlineEdits;
   answers containing edits gain an "Apply N tracked edits" button that
   drives the same applyTrackedEdits() search-and-replace-under-
   TrackAll path as the Actions tab, with the same honest
   applied/skipped reporting. Streaming messages are never parsed - a
   half-received REPLACEMENT must not reach the document.
3. Because parsing keys off the answer's shape rather than the toggle,
   edits proposed spontaneously by the model are applyable too.

Also: PW_VIDEO=1 makes Playwright record a webm per test (off by
default - videos slow the suite and bloat CI artifacts) so add-in
behavior can be reviewed as short clips without running anything.

TESTING
Three new hermetic specs: the redline switch forces document context
and appends the contract invisibly (request-body assertion + transcript
must not leak it); chat-proposed edits apply as tracked changes with
exact recorded Word calls; plain prose answers offer inserts but no
apply button. Suite: 64/64.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
b3ed17c2 test(word-addin): live full-stack demo suite + real Word-on-the-web driver Amalanand Muthukumaran 2026-08-03 ↗ GitHub
commit body
WHY THIS MATTERS
The hermetic e2e suite proves the task pane against a mocked Office.js
host with mocked backends - fast and deterministic, but it can never
answer "does the real integration work?". This adds the opposite end of
the testing spectrum: everything real except what physically cannot be
real in the environment.

WHAT'S IN THE BOX
1. playwright.live.config.ts + e2e-live/live-demo.spec.ts - LIVE runs:
   real Supabase sign-in through the dev server's HTTPS proxy, real Mike
   backend, real streamed model responses; only the Word JS API surface
   is shimmed (a plain browser has no Office host). Assertions target
   the redline CONTRACT (parseable edits → Apply button → recorded
   TrackAll replacements), not exact model wording, so the suite
   tolerates model nondeterminism. Video recording is always on - the
   runs double as review reels.
2. e2e-live/word-web-session.mjs - drives the add-in inside REAL Word
   on the web. Word online needs a signed-in Microsoft account no
   automation can create, so a persistent profile splits the work:
   --login opens office.com for a one-time manual sign-in (detected by
   the marketing page's Sign-in button disappearing - logged-out
   office.com does NOT redirect to login), --record then reuses the
   session to open a document, type a demo contract, sideload
   manifest.xml, sign into Mike, and apply chat-proposed redlines,
   recording video + step screenshots to the Desktop.
3. webpack.config.js: DEV_HTTPS_CERT/DEV_HTTPS_KEY serve existing cert
   files directly. office-addin-dev-certs otherwise installs its CA
   into the OS keychain via an admin prompt no automated environment
   can approve; the driving browser tolerates the untrusted cert with
   --ignore-certificate-errors instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
412d3529 fix(word-addin): --login window closed before the user could sign in Amalanand Muthukumaran 2026-08-03 ↗ GitHub
commit body
The login waiter's done-signal was "the page left login.microsoftonline.com"
- but the flow STARTS on the logged-out office.com marketing page, which is
already off that domain, so the predicate was instantly true and the window
closed before the user touched the keyboard.

Now --login opens the real login flow and polls every 5s for an actual
signed-in state (the marketing page's Sign-in buttons disappearing),
deliberately never re-navigating while the user is on a login/signup domain
mid-flow, and exits non-zero if the window is closed or 10 minutes pass
without a session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
b039be86 fix(word-addin): word-web recorder works end-to-end against real Word online Amalanand Muthukumaran 2026-08-03 ↗ GitHub
commit body
Verified live: this now records the full journey in real Word on the web
- new document, contract typed, manifest sideloaded, Mike pane opened,
sign-in, chat-proposed redlines applied as genuine tracked changes
(strikethrough + insertion, Accept/Reject live in the Review ribbon).

Each fix below came from live DOM recon, not guesswork:

1. EDITING CANVAS. '[contenteditable="true"]' matched Word's off-screen
   screen-reader navigation table first; the real surface is
   #WACViewPanel_EditingElement inside the WacFrame_Word_0 iframe. Typing
   also lands silently in the void while the editor is still booting, so
   the script now types INTO the canvas locator and verifies the text
   actually appears, retrying once.
2. ADD-INS DIALOG PATH. Ribbon #InsertAddInFlyout (retried - clicks are
   swallowed during ribbon init) → "More Add-ins" → the Office Add-ins
   dialog in a cross-origin iframe named "_xdm_*" → MY ADD-INS tab →
   "Manage My Add-ins" dropdown. The dropdown's visible "Upload My
   Add-in" item is a NEW element; the always-present #UploadMenuInner
   anchor stays hidden forever (a decoy locators must skip).
3. LOCALHOST TASK PANE BLOCKED SILENTLY. The pane iframe existed with
   src https://localhost:3000/taskpane.html but never issued a network
   request: Chrome's Local/Private Network Access checks block a public
   origin (word-edit.officeapps.live.com) from iframing localhost -
   exactly what a sideloaded dev add-in is. The launch args now disable
   those checks for the demo profile.
4. Failures screenshot + dump every frame's labelled controls to
   fail-<step>.json before exiting non-zero, which is how each of the
   above was diagnosed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
b29e56cd test(word-addin): all-buttons Word-online demo recorder + keyless model stand-in Amalanand Muthukumaran 2026-08-03 ↗ GitHub
commit body
WHY THIS MATTERS
The existing word-web recorder (word-web-session.mjs) demonstrates only the
chat redline flow. Reviewers asked for proof that EVERY actionable control
in the pane works against real Word on the web - including the two insert
buttons and both Improve Writing replace modes that no earlier video showed.
A reproducible recorder is better than a one-off screen capture: anyone can
re-run it and get the same evidence.

WHAT IS IN HERE
- e2e-live/word-web-full-demo.mjs: drives real Word online (same persistent
  profile + sideload path as word-web-session.mjs) through every button:
  chat "Apply N tracked edits" / "Insert below cursor" / "Insert below
  (tracked)", Improve Writing (tracked + plain replace), Proofread apply,
  Anonymise apply, Draft Clause (both inserts), Workflows run + insert,
  Projects tab, and Sign out. Each model flow targets DIFFERENT contract
  text so the tracked changes never collide and the "Apply 2 ..." labels are
  deterministic.
- e2e-live/anthropic-stub.mjs: a minimal Anthropic Messages API stand-in
  (SSE streaming protocol only) with scripted responses per prompt shape.
  Lets the FULL stack run - real Supabase auth, real backend, real SSE,
  real Word JS API - when no funded ANTHROPIC_API_KEY is available. Start
  it on :4141 and launch the backend with
  ANTHROPIC_BASE_URL=http://127.0.0.1:4141.

HOW THE RECORDER WORKS (and one hard-won lesson)
Playwright coordinate clicks (locator.click) inside the task pane silently
NO-OP: the pane is a doubly-nested cross-origin iframe
(word.cloud.microsoft -> officeapps _xdm_ frame -> localhost:3000) and
Chromium mis-routes compositor input for it, while Playwright's
actionability checks still pass. fill(), canvas clicks and the add-ins
dialog are unaffected. The paneClick() helper therefore dispatches DOM
click events via locator.evaluate(el => el.click()) for every pane control.
926af70c feat(word-addin): unify the pane's look with the web assistant's design system Amalanand Muthukumaran 2026-08-03 ↗ GitHub
commit body
WHY THIS MATTERS
The add-in and the web app are one product, but the pane previously spoke a
different visual dialect (flat shadcn primitives, sans-serif answers, raw
ORIGINAL/REPLACEMENT text blocks in the transcript) while the web assistant
uses glass surfaces, EB Garamond serif prose, edit cards, and activity
event rows. A lawyer moving between Word and the browser should see ONE
interface. Fonts were already shared (Inter + EB Garamond via the vendored
tokens); this commit closes the remaining gap.

WHAT IS DUPLICATED (deliberately)
src/taskpane/components/assistant/ mirrors
frontend/src/app/components/assistant/ with the network/DOM concerns the
pane doesn't have stripped out:
- EditCard: reason line + serif diff slab (replacement green, original red
  strikethrough). Informational in the pane: after applying, Word's own
  Review ribbon owns accept/reject, so the card shows applied/skipped
  instead of Accept/Reject buttons.
- EditCardsSection: white glass container with "N tracked changes" summary,
  chevron collapse, and an actions row (the pane's Apply pill sits where
  the web's "Accept all / Reject all" go).
- PreResponseWrapper: the collapsible "Working ..." / "Completed in N
  steps" strip shown before an answer.
- EventBlocks: dot-and-connector rows - "Read Current document" when the
  pane reads the file before asking, and "Found "..." (N matches)" rows
  driven by the REAL Word search results from applyTrackedEdits (the hook
  now reports per-edit match counts).
- PillButton (black/white/blue/danger tones) and UserMessage (gray
  right-aligned bubble).
The add-in intentionally duplicates rather than imports these: it ships
standalone (own bundle, no Next.js), and UI parity is a product decision
reviewed per surface, not an accidental coupling.

HOW THE TRANSCRIPT CHANGED
Chat answers render as serif prose with the machine-readable
ORIGINAL/REPLACEMENT/REASON blocks STRIPPED (new stripRedlineBlocks in
lib/redline.ts) - the same content the user needs is presented as edit
cards instead, exactly like the web. Proofread and Anonymise results get
the same card treatment once their stream completes; while streaming, raw
text still shows so progress stays visible.

WHAT DID NOT CHANGE
Every button label, aria-label, placeholder, and status string is
byte-identical - the hermetic Playwright suite (64 specs) passes with zero
test edits, and the live demo recorders keep working unmodified.
aa234ba9 fix(backend): use main's spotlighting helpers after rebase - drop duplicates Amalanand Muthukumaran 2026-08-03 ↗ GitHub
commit body
WHY THIS MATTERS
This branch predates upstream's security-posture work, and both sides
independently added the same two prompt-injection helpers
(generateSpotlightNonce, spotlight) to contextBuilders.ts in DIFFERENT
regions of the file. Git therefore auto-merged the branches with no
textual conflict - but the merged module declared each function twice,
which is a parse error. That single file broke three CI checks at once:
11 backend test suites (vitest couldn't load the module), the Playwright
job (the API server couldn't boot, so the health-check wait timed out),
and its dependent CodeQL gate.

WHAT IS A SEMANTIC MERGE CONFLICT
Git resolves merges line-by-line, not meaning-by-meaning. Two additions
that don't touch the same lines merge "cleanly" even when they are
mutually incompatible - duplicate declarations, a renamed function still
called by new code, etc. Green CI on each branch alone proves nothing
about the MERGE of the two; that is why PR checks run against a merge
ref with the target branch.

HOW THE FIX WORKS
Keep upstream's versions (they are a superset of ours: identical fence
format and signature, plus neutralization of the <workflow-instructions>
fence family) and delete this branch's copies. The add-in-specific pieces
(parseOptionalDocumentContext, buildWordDocumentContextPrompt,
MAX_DOCUMENT_CONTEXT_CHARS) stay, now calling the shared helpers.
Verified on the rebased tree: tsc clean, 437 backend tests pass -
including upstream's spotlight suites and this branch's documentContext
suite exercising the same code path.
0cfd362d fix(word-addin): hostname-anchored login detection in the demo recorders Amalanand Muthukumaran 2026-08-03 ↗ GitHub
commit body
WHY THIS MATTERS
CodeQL flagged both e2e-live recorder scripts with
js/incomplete-url-substring-sanitization (high): they decided "are we on
the Microsoft login page?" with url.includes("login.microsoftonline.com").
A substring test matches that string ANYWHERE in the URL - including
attacker-shaped hosts like login.microsoftonline.com.evil.test or benign
pages whose path merely embeds the domain - so it is not a safe way to
classify a URL's origin. These are local demo drivers, so the practical
risk is low, but the pattern is exactly what the rule exists to catch and
scripts get copied.

HOW THE FIX WORKS
Parse the URL and compare its HOSTNAME: exact match or a dot-anchored
suffix (hostname === domain || hostname.endsWith("." + domain)) against
the two real login hosts. The login-mode wait loop keeps its "don't
re-navigate while the user is typing" behavior via the same helper plus a
case-insensitive signup check.
b29a8583 test(word-addin): align live-demo contract with the per-flow stub targets Amalanand Muthukumaran 2026-08-03 ↗ GitHub
commit body
WHY THIS MATTERS
The Anthropic stand-in scripts each add-in flow against DIFFERENT document
flaws so tracked changes never collide when one document exercises every
feature (chat: Suplier/reciept, proofread: Schedual/writen). The live demo
spec's contract still carried the older correctly-spelled variants, so the
stubbed Proofread corrections found no match in the mocked document: the
apply reported "0 of 2" and the spec's tracked-changes assertion failed.
A fixture and the responses scripted against it must evolve together.

HOW THE FIX WORKS
The spec's contract now contains the same two proofread targets the stub
proposes ("Schedual 1", "writen notice"), restoring 3/3 in
playwright.live.config.ts runs (verified locally, real Supabase + backend).
08f53486 fix(word-addin): harden the local dev experience against its three silent traps Amalanand Muthukumaran 2026-08-03 ↗ GitHub
commit body
WHY THIS MATTERS
A cloner's first hour with the add-in is dominated not by the code but by
three failure modes that give no useful error. All three were hit in real
use on a machine that had run the add-in before; each now either
self-heals or is documented with its exact recovery.

TRAP 1 - CERTIFICATE TRUST DRIFT (the nastiest)
The dev certificate expires after ~30 days, and the tooling silently
regenerates it WITH A NEW SIGNING CA (the webpack dev server triggers
this on startup). The OS keychain still trusts only the old CA, so
desktop Word rejects the pane with an opaque "content is blocked because
it isn't signed by a valid security certificate" - while
`office-addin-dev-certs verify` reports "trusted", because it only checks
that a CA *by that name* exists, not that it signed the current cert.
Worse, `install` then refuses to reinstall for the same reason.
Fix: scripts/dev.sh now asks the OS for its verdict on the ACTUAL leaf
(`security verify-cert -c localhost.crt -p ssl -s localhost` on macOS;
tool fallback elsewhere). On drift it forces uninstall→install, falls
back to trusting the current ca.crt directly, re-verifies, and exits
non-zero with a README pointer instead of launching into a broken Word.
README Troubleshooting documents the by-hand recovery.

TRAP 2 - STALE SIDELOAD REGISTRATION
office-addin-debugging registers the add-in by hard-linking manifest.xml
into Word's wef folder; a crashed run leaves the link behind and the next
`npm start` dies with an opaque EEXIST. New scripts/clear-sideload.js runs
as the npm `prestart` hook: best-effort deregistration before every
start, cross-platform, and deliberately never fails (a broken stop must
not block start - start surfaces the more actionable error).

TRAP 3 - WORD ON THE WEB LOADS NOTHING, SILENTLY
Word online's editor frame is a public origin; Chrome's Local Network
Access checks block it from iframing https://localhost:3000 with no
visible error - the pane simply never appears. Deployed add-ins are
unaffected; dev sideloads are dead on arrival. New
e2e-live/manual-session.mjs launches a browser with those checks
disabled, reuses the persistent Microsoft profile, sideloads the
manifest, opens the pane, and hands the window over for manual testing.
The README's Word-on-the-web section now leads with this caveat.

ALSO IN THE README
- Prerequisites now state up front that a Mike user account and a funded
  LLM key (or the keyless stand-in) are needed - previously discovered
  only at first sign-in / first chat.
- New "Testing without an LLM key" section documents
  e2e-live/anthropic-stub.mjs: a local server speaking the Anthropic
  streaming protocol with scripted answers, so the full real stack
  (Supabase auth, backend, SSE, Word tracked changes) runs without any
  API spend.
- Troubleshooting gains entries for the EEXIST trap and the port-3000
  collision with the web app's dev server.
5d1d0301 fix(chat): make the Word document field snake_case and single-nonce Amal 2026-08-04 ↗ GitHub
commit body
Two contract fixes surfaced by comparing the add-in endpoint against the
rest of POST /chat:

- The wire field becomes `document_context`, matching every other
  multiword field on this route (chat_id, displayed_doc,
  attached_documents). The old `documentContext` spelling is still
  accepted as a deprecated alias because shipped add-in builds send it;
  new clients should use the snake_case form.

- buildWordDocumentContextPrompt now takes the per-request nonce instead
  of minting its own. The system prompt tells the model to distrust any
  fence tag WITHOUT the current nonce, so a request must carry exactly
  one nonce - a second fence nonce made the Word document block fail the
  model's own verification rule.

Tests follow the route's conventions: the new integration cases assert
the 400 shape, the alias, and - via the buildMessages call - that the
document fence carries the same nonce as the rest of the request. The
spotlight test files move from lib/chat/__tests__ into the flat
lib/__tests__ directory where every other lib test (including this
feature's own documentContext.test.ts) already lives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6ae25260 feat(word-addin): run workflows and actions the way the web app does Amal 2026-08-04 ↗ GitHub
commit body
The pane previously pasted workflow skill_md - and the Actions tab pasted
the whole document body - directly into the user message. The web app
never does either: a workflow travels as a `{ id, title }` reference the
backend resolves server-side (inside the semi-trusted
<workflow-instructions> fence), and document text reaches the model only
through nonce-fenced context blocks.

Pasting the body client-side quietly upgraded its trust level: a shared
workflow with a hostile body, or a document with embedded instructions,
arrived as the user's own words and bypassed the spotlighting the backend
builds for exactly this content. It also guaranteed behavioral drift -
server-side improvements to a workflow's prompt would never reach the
pane.

Now:
- WorkflowPicker sends the workflow reference on the message and lets the
  backend fetch and fence skill_md, same as the web.
- Proofread/Anonymise send the document via `document_context` (fenced
  server-side) with an instruction-only user message.
- The client uses the route's snake_case `document_context` field.

E2E request-body assertions updated to pin the new wire shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
49eebd32 style(word-addin): mirror the web app's composer, toggles, and tab pills Amal 2026-08-04 ↗ GitHub
commit body
The pane's chrome had drifted into a second visual vocabulary: a flat
shadcn composer with a circular send button, a Radix switch with a black
track, and underline tabs - where the web app uses a liquid-glass
composer with a gradient-black square action button, a hand-rolled
blue-track ToggleSwitch, and glass pill tabs. Same product, two looks.

This restyles the pane to the web's exact class strings:
- ChatInput copies the web composer's glass container and single
  send/stop action button (frontend .../assistant/ChatInput.tsx).
- toggle-switch.tsx and tab-pill-button.tsx are faithful copies of the
  web components (only the cn import path differs), replacing the Radix
  switch - whose dependency is dropped - and the underline tab bar.
- ChatBubble.tsx is deleted: it was imported nowhere and its styling
  contradicted the real UserMessage; left alone it would have fossilized
  into the future shared package as the wrong bubble.
- The Markdown and tokens.css headers no longer claim the web app
  consumes them - it doesn't yet; that claim belongs to the shared
  package extraction, which these copies are staged for.

Behavior is unchanged: aria roles/labels (tab, switch, Send/Stop) are
preserved, so the full e2e suite passes as-is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7578545b fix(word-addin): match the stub's proofread response to the new prompt Amal 2026-08-04 ↗ GitHub
commit body
The proofread action no longer pastes the document into the user turn -
the prompt is instruction-only ("provided as document context") and the
body travels via the fenced document_context field. The live-demo stub
picked its scripted redlines by matching the old prompt text, so the
full-button Word-online demo would stream the generic summary instead of
corrections. Verified end-to-end against real Word on the web: all 16
demo steps pass, tracked changes land in the document.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
67a8d54e fix(word-addin): remove the guest sign-in flow that calls a nonexistent endpoint Amal 2026-08-05 ↗ GitHub
commit body
WHY THIS MATTERS
The login page rendered a "Continue as guest" button in every
non-production build. Clicking it POSTed to `${API_BASE_URL}/auth/guest`
- an endpoint that does not exist anywhere in this repository (the
backend has no auth router at all; authentication is delegated entirely
to Supabase's token grants). So the button could never succeed: every
click ended in a fetch error dressed up as "Guest login is unavailable".
Worse, the comments around it claimed the endpoint "mirrors the web app"
and "is gated to non-production on the server too" - statements that are
simply false for this codebase. Dead UI is bad; dead UI with
authoritative-sounding comments is a trap for the next contributor, who
will reasonably go hunting for a server route that was never there.

WHAT IS DEAD-ENDPOINT DRIFT
Client code often outlives (or predates) the server contract it was
written against. When a client keeps calling a route the server never
implements, you get "dead-endpoint drift": the code typechecks, the UI
renders, and the failure only shows up as a runtime network error - the
compiler cannot save you because HTTP paths are just strings:

  // Typechecks fine. Fails 100% of the time at runtime:
  await fetch(`${API_BASE_URL}/auth/guest`, { method: "POST" });

The only defenses are (a) not shipping calls to routes you haven't
built, and (b) comments that describe what IS, not what you wish were.

HOW THE FIX WORKS
The whole guest path is deleted end-to-end so no layer references it:

  - session.ts: `signInAsGuest()` and the now-unused API_BASE_URL
    constant are removed (the session module talks only to Supabase;
    the Mike API base belongs to api/mikeApi.ts).
  - useAuth.ts: the `loginAsGuest` binding is dropped from AuthState.
  - LoginPage.tsx: the guest button, its NODE_ENV gate, and the false
    "server-gated" comment are gone; the page now offers exactly the
    one flow the backend supports - Supabase email/password sign-in.

If a real guest endpoint lands server-side someday, the flow can be
reintroduced against the actual contract instead of an imagined one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
b87520fe fix(word-addin): abort workflow runs on unmount and add a visible Stop control Amal 2026-08-05 ↗ GitHub
commit body
WHY THIS MATTERS
WorkflowPicker's handleRun started a server-sent-events (SSE) chat
stream with no AbortSignal and no mounted guard. Switching tabs mid-run
therefore (1) left the HTTP stream open, so the backend kept the LLM
call running and kept billing the user's tokens for an answer nobody
would ever see, and (2) let every later chunk call setState on an
unmounted component - wasted work and a classic React leak pattern.
ChatPanel and DocumentActions already implement the correct discipline;
this brings the remaining two panels in line, and gives the user a
Stop button so a run started by mistake can be cancelled at all.

WHAT IS THE ABORT/MOUNTED DISCIPLINE
Two refs plus one cleanup effect:

  const abortRef = useRef<AbortController | null>(null);
  const mountedRef = useRef(true);
  useEffect(() => {
    mountedRef.current = true;
    return () => {          // runs when the tab unmounts the panel
      mountedRef.current = false;
      abortRef.current?.abort();   // tears down the fetch + SSE reader
    };
  }, []);

`AbortController.signal` is threaded into fetch; aborting it closes the
connection, which is the backend's cue to cancel the upstream LLM call.
`mountedRef` guards every setState that can fire after an await, because
a promise resolving is not evidence the component still exists. In the
catch block, `controller.signal.aborted` distinguishes a deliberate
stop (keep the partial text, show no error) from a real failure.

HOW THE FIX WORKS
  - WorkflowPicker: handleRun now creates an AbortController per run,
    passes its signal to streamAssistant, guards all setState with
    mountedRef, and treats an aborted run as "keep the partial result".
    While a run is streaming, the Run button is swapped for a Stop
    button (same pattern as ChatPanel's composer Stop), wired to
    abortRef - the visible cancel the panel never had. The initial
    listWorkflows() load gets the same mounted guard.
  - ProjectPicker: the upload flow gets a mountedRef guard so an upload
    resolving after a tab switch no longer setStates on an unmounted
    component. The upload request itself cannot be aborted mid-flight -
    the shared uploadProjectDocument() accepts no AbortSignal (and the
    vendored client is deliberately untouched) - but unlike an LLM
    stream an upload is cheap, so discarding the response suffices.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
92851765 fix(word-addin): insert the stripped prose, not raw redline scaffolding Amal 2026-08-05 ↗ GitHub
commit body
WHY THIS MATTERS
When a chat answer contains parsed redline edits, the transcript is
careful to render `stripRedlineBlocks(msg.content)` - the prose with the
ORIGINAL:/REPLACEMENT:/REASON: blocks removed, because those blocks are
machine-facing scaffolding that drives the "Apply tracked edits" flow.
But the "Insert below cursor" / "Insert below (tracked)" buttons still
inserted the RAW `msg.content`. One click would paste something like

  ORIGINAL: The Party shall indemnify...
  REPLACEMENT: The Party will indemnify...
  REASON: plain-language style

verbatim into a legal document - exactly the internal format the UI
goes out of its way to hide. What you see should be what you insert.

WHAT IS THE STRIP/PARSE SPLIT
A redline-mode answer is one string serving two consumers:
  - parseRedlineEdits(content)  -> structured edits for the Apply flow
    (Word search + tracked replacement);
  - stripRedlineBlocks(content) -> the human prose around the blocks,
    for reading and for insertion as text.
Every code path must pick the representation matching its purpose; the
insert buttons had grabbed the un-split original by mistake.

HOW THE FIX WORKS
The render already computes exactly the right value:

  const prose =
    edits.length > 0 ? stripRedlineBlocks(msg.content) : msg.content;

The insert buttons now receive `prose` instead of `msg.content`, and
their visibility condition changes from `msg.content && isComplete` to
`prose && isComplete` - so an answer that is ALL edit blocks (empty
prose) offers only the Apply flow, instead of an insert button that
would throw "There is no text to insert" after stripping. The Apply
button intentionally keeps `msg.content`: parseRedlineEdits needs the
blocks it parses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
a2746c66 fix(chat): drop the documentContext alias no shipped client ever used Amal 2026-08-05 ↗ GitHub
commit body
WHY THIS MATTERS
POST /chat accepted the document body under two spellings:

  body.document_context ?? body.documentContext

with a comment claiming the camelCase alias protected "add-in builds
that shipped before the field was aligned". But no such builds exist:
word-addin/ is introduced by this very PR, and its vendored client has
sent snake_case `document_context` from the start. The alias guarded
against a past that never happened - and compatibility shims are not
free. Every alias doubles the surface a validator, a security fence,
and future maintainers must reason about, and once an alias ships,
external callers CAN start depending on it, turning a fictional
obligation into a real one.

WHAT IS API SURFACE MINIMALISM
An HTTP body field is a public contract. The rule of thumb: accept
exactly one spelling per field, and only add an alias when a concrete,
identifiable client population depends on the old one. "Someone might
have..." is not such a population - version control history is. If a
legacy spelling genuinely exists in the wild you keep it with a dated
deprecation note; if it provably doesn't, the kindest thing for the
contract is to never let it exist.

HOW THE FIX WORKS
The route now parses only the documented snake_case field:

  const parsedDocumentContext = parseOptionalDocumentContext(
      body.document_context,
  );

and the integration test that pinned the alias ("accepts the deprecated
documentContext alias") is deleted with it - a test whose only job was
to keep dead surface alive. The snake_case tests (400 on non-string,
nonce-fenced injection into the system prompt) still cover the field's
real contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
74a32df9 refactor(word-addin): delete the caller-less readDocumentOoxml helper Amal 2026-08-05 ↗ GitHub
commit body
WHY THIS MATTERS
useWordDoc exported `readDocumentOoxml()` - a wrapper around Word's
body.getOoxml() - but nothing in the add-in ever called it. Its only
"usage" was the e2e Office shim faithfully counting getOoxml() calls in
an `ooxmlReads` counter that no test asserted on. Dead exports are not
harmless: each one is API surface a reader must understand, a mock
author must simulate (as happened here - the shim grew a whole fake
OOXML serializer for it), and a refactorer must preserve "just in
case". The mock tracking is the tell: test scaffolding that exists only
to observe code that exists only to be observed.

WHAT IS DEAD-CODE GRAVITY
Unused code attracts more unused code. The export looked load-bearing,
so the e2e mock implemented getOoxml(), which made the export look
USED (it appears in grep!), which is precisely how dead code survives
review after review. The way out is to trace real call graphs from
product code, not from test doubles: a symbol whose only references
live in mocks and their bookkeeping has zero callers.

HOW THE FIX WORKS
  - useWordDoc.ts: `readDocumentOoxml` is removed from the hook body
    and its returned API. The genuinely used readers remain -
    readDocumentText() for prompt context and getDocxBlob() for
    uploads (which gets real binary .docx bytes via getFileAsync, not
    OOXML).
  - e2e/support/office-mock.ts: the getOoxml() fake, the `ooxmlReads`
    counter (interface field + initializer + increment), and the
    docstring mention are deleted, so the shim once again models only
    what the add-in actually touches.

If OOXML-level reads are ever needed (e.g. style-preserving export),
the helper is one Word.run() away - with a caller to justify it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-1306.md from inside the repo you want the changes in.

⬇ Download capture-thread-1306.md