# Composer ligature proof matrix — pass 1

PR [#27909](https://git.cafe/pingdotgg/t3code/pull/27909), issue [#27895](https://git.cafe/pingdotgg/t3code/issues/27895).
Reviewed head [ab17d3b](https://github.com/pingdotgg/t3code/commit/ab17d3b8b1295170213b7537dddcf00dadee7708), branch `fix/issue-27895`; comparison base [be5a887](https://github.com/pingdotgg/t3code/commit/be5a887b939dadf74e62a477bbe2c805d561fe9e).
Read-only source inspection in `/home/ubuntu/wt/pr-27909`, 2026-11-03. This is a capture specification, not visual verification. Every evidence status below is **MISSING** unless explicitly excluded. The PR body's older-head images, videos, drivers, and third-party desktop report are hints only; none is accepted as current proof.

## Findings

1. **Current proof is absent.** Capture the exact comparison revisions above. Do not reuse the older PR-body before/after pair or its claims of pixel equality.
2. **Two production callers, three placements.** The rule matches the chat editor and `PromptFontPreview`. The preview appears beneath Interface font in simple typography and beneath Prompt font in Separate fonts mode. Testing chat alone misses an affected surface.
3. **Inheritance broadens the visual scope.** Both properties inherit into prose, rich marks, block text, chips, and editable code descendants unless a descendant overrides them. The code font differs from the prompt font. A no-change result with default monospace does not establish that every custom ligating code font is unchanged. This is a coverage requirement, not a demonstrated defect.
4. **Record editing and mobile expansion.** Static arrows cannot prove caret stops, selection/deletion inside a shaped arrow, raw ASCII preservation, or the collapsed/expanded handoff. The collapsed mobile label is a separate button outside the selector.
5. No source-level blocker found in the six-line diff. The two-class, unlayered rule outranks Tiptap's injected one-class rule. Source inspection cannot establish actual glyph shaping or browser caret behavior.

## Fixture matrix

`W` = Linux Chromium web, 1440×1000 CSS px, fine pointer. `N` = Linux Chromium mobile web, 390×844 CSS px, coarse pointer/touch. `E` = real Linux Electron renderer, 1440×1000 content viewport (record actual dimensions if window framing differs). All use DPR 1, 100% zoom, default sizes unless specified. `L`/`D` = built-in light/dark. Each listed capture is a **base/head pair**, with identical data, fonts, viewport, theme, and focus state. Recording rows need video on both revisions; include stable start/end frame crops as well. A still alone does not satisfy them.

| ID / named fixture | Required data and state | Diff affects it? | Minimum viewport × theme × row; media | Real-app seed / driver | Pass-1 status |
|---|---|---|---|---|---|
| A — Reporter, matched Inter, short chat prose | Interface **and Prompt** font explicitly `Inter Variable`; rich text on; sent message and unsent prompt both `a -> b => c`; no pending requests, attachments, or menus | Yes, editor glyphs/advance; sent message is unchanged control | W×L×A, W×D×A, N×L×A, N×D×A; still pairs. Also E×L×A still pair for the reported desktop surface. **Recording** W×L×A: type both sequences, click/caret through each constituent, Shift-select one constituent, replace/delete/undo/retype, copy and send. N×D×A **recording**: collapsed label → expand → type → blur/collapse → reopen. | Seed a stopped synthetic thread and its sent text via S1 below; fill unsent draft through the real editor. Open `/{environmentId}/{threadId}`. Use real desktop for E; a browser with an Electron user agent is not E. | **MISSING**, all five still pairs and both flow pairs |
| B — Untouched default font fallback | New client preferences: `fontFamilySans=""`, `fontFamilyComposer=""`; same ASCII prompt/sent text as A; rich on; default 14px prompt | Yes if the actual fallback shapes the sequences; always changes root computed properties | W×D×B, still pair | Fresh settings/profile before setting any font; S1. Record actual rendered font, not just CSS stack. Bundled Inter follows Apple system faces in the default stack. | **MISSING** |
| C — Independent prompt override, narrow wrap and overflow | Interface `Liberation Sans`, Prompt `Inter Variable`, Prompt size **20**; 12 lines of `office affine fi fl — a -> b => c`, plus one long unbroken repetition. Rich on; enough text to exceed editor max-height | Yes, prompt shaping can move wrap points/scroll height; sent text intentionally uses a different family | N×D×C, **recording**, top → middle → bottom → caret at end. Capture initial/final scroll positions consistently | Appearance → Separate fonts → Interface/Prompt font and Prompt font size. S1, paste the fixed multiline string. Verify overflow and no horizontal clipping; record actual CSS size and coarse-pointer match. | **MISSING** |
| D — Rich marks, blocks, tokens, default code font | Rich on, prompt Inter. Markdown sample D below: plain/bold/italic/strike, headings, quote, bullet/ordered/task lists, rule, file link, known skill, inline code and TypeScript fence; default code font. Same source in sent control | Yes for inherited prose/marks; code/chip labels are inheritance controls with their own styles | W×L×D, **recording** through scroll, moving caret into/out of marks, range selection across a token, and toggling a task checkbox. Capture readable crops of every content class | S1 + real Markdown paste; S2 gives a deterministic known skill and file chip. A context-reference/citation node can be a labelled harness supplement (H1) when no deterministic real-app seed is available. | **MISSING** |
| D2 — Ligating monospace sentinel | Same short prose + inline/fenced code sample, with a genuinely installed ligating monospace code font (e.g. Fira Code); actual font and version recorded. Include `-> => != ===` and ordinary `fi fl` | Potentially yes in code descendants; do not assert all code remains pixel-identical | W×L×D2, still pair with code crops; may reuse D's recording if the font is changed within that recording | Appearance Code/Monospace font. This host's installed-font listing contains DejaVu/Liberation but no Fira Code. Use a capture host with the font, or **labelled harness capture** loading a licensed, pinned font asset. A silent fallback is not this row. | **MISSING**; font prerequisite identified |
| F — Plain mode, empty and restored draft | Rich off; Inter matched; source `**a -> b** and \`c => d\` $frontend-design [surface.test.ts](apps/web/src/terminal/ghostty/surface.test.ts)`. Formatting markers stay literal, while supported tokens can still become chips | Yes, same root despite the plain extension set | W×L×F, **recording**: rich → plain toggle, edit across arrows, clear to empty placeholder, restore via undo/history/stash, switch away/back to the thread | `/settings/chat`, `Rich text composer`; S1 + S2. Use A's basic arrow editing recording where equivalent; preserve a visible plain-mode checkpoint. Empty placeholder is a sibling control, not an expected ligature change. | **MISSING** |
| G — Literal pending answer, editable → readonly | `literalText=true`, `richTextEnabled=true` (literal wins), value `**a -> b** $frontend-design => c`, empty context records, cursor at end; transition disabled false → true → false. Syntax and skill text remain literal | Yes in text; disabled changes editability/ARIA, not the font selector | W×L×G, **recording**, labelled harness unless a real pending request with `initialAnswer` is deterministically supplied | H1. Chat sets literal mode when active question has `initialAnswer`; readonly also occurs while responding. Stock Replay `question` fixture is an enum question without a default and does **not** establish this literal case. No deterministic public-RPC seed for a pending literal request was established in this audit. | **MISSING**; harness seed specified |
| P — Simple Appearance preview | Separate fonts off; Interface Inter, Prompt override empty; stock preview with known skill and file links, append ` a -> b => c` | Yes, second direct caller; preview defaults to plain editor extensions | W×L×P, **recording** of typing arrows in `Prompt font preview`, caret/edit, and clearing to its placeholder. Stable before/after frame at full stock text + arrows | `/settings/appearance`; stock `PromptFontPreview` is editable, local component state. Seed through its textbox. Keep stock skill/links to show token adjacency. | **MISSING** |
| Q — Separate fonts preview and non-ligating prompt | Separate fonts on; Interface `Liberation Sans`, Prompt `Inter Variable`; stock preview + arrows. Then change Prompt to `Liberation Mono`, and reset it to fallback | Yes for inherited feature settings; glyph response depends on selected font. The preview moves under Prompt font | W×D×Q, **recording**, checkpoints at Inter override, non-ligating mono, and reset. Include corresponding chat draft checkpoints in the same recording | `/settings/appearance` → Advanced → Separate fonts. Use actual font controls. This checks independent prompt inheritance and reversible setting changes; match sent/control fonts only when claiming identical glyphs. | **MISSING** |
| X1 — Placeholder / empty / choice-only / no project | Empty value; placeholder varies for normal, approval, pending choice, plan feedback, missing project/provider; no-context vs context-only; disabled as applicable | Root properties change, but placeholder is a sibling and contains no affected editor text; collapse into empty checkpoint in F/P | **No additional capture** beyond F/P empty checkpoints. Disabled arrow text covered by G | App state or H1 for choice-only/missing-project; do not fabricate a screenshot by mutating DOM | **EXCLUDED from additional minimum**, code-derived equivalence |
| X2 — Outside-selector UI / absent editor | Route loading/missing/deleted; composer not yet mounted; attachments and queued edit banners; error/resume/approval/plan UI; suggestion/stash menus; native mobile composer; sent messages | No direct selector match on these surfaces. Surrounding UI is a control; native mobile is separate React Native | No independent pair; preserve full frames around affected rows so unexpected layout movement can be seen | No app seed needed for exclusion; see branch inventory | **EXCLUDED**, no new rendering implementation in diff |

The minimum is **13 still checkpoints per revision** (A×5, B, D2, and C/D/F/G/P/Q×1) plus recordings for A's desktop-web edit flow, A's narrow expand/collapse, and C/D/F/G/P/Q. Recording checkpoints can supply their corresponding static captures; do not duplicate whole sessions. D2 can share D's recording/session when the font prerequisite is met. Each named row is required once at its specified tuple; no full Cartesian product of all rows with both widths/themes is required.

## Seed specification

**S1 — real app.** Use the lead/capturer's isolated app state, never the live T3 database. Create a synthetic git workspace with one commit and a branch so the composer context strip is stable. Via authenticated app WebSocket RPC, `projects.mutate` with `type: "project.create"` creates its project; `orchestration.dispatchCommand` with `thread.create` creates a thread using an available provider instance/model; `message.dispatch` with `dispatchMode: {type: "defer_start"}`, synthetic `messageId`, text, and `attachments: []` seeds the sent body, then `thread.stop` prevents provider execution. Await receipts/projections before opening the route. The current contracts contain these commands. Do not copy the old PR-body request envelope blindly. Pick an available model from the running instance registry instead of hardcoding the older driver's Codex model. The draft can be filled through the real textbox/paste path. To exercise Send use Replay (dev), not a paid provider. A new-thread draft from the app's New thread action uses `/draft/{draftId}` and the same editor; use it as the restoration checkpoint in F if convenient.

**S2 — deterministic tokens.** Create the synthetic `apps/web/src/terminal/ghostty/surface.test.ts` file beneath that workspace root and provide one known invocable skill, `frontend-design`, through the isolated provider's actual discovered skills (a fixture skill file under its supported discovery directory is sufficient). File-link Markdown is `[surface.test.ts](apps/web/src/terminal/ghostty/surface.test.ts)`; skill text is `$frontend-design`. If that skill cannot be supplied through the selected provider, use the labelled H1 skill fixture instead. The stock Appearance preview already supplies a deterministic skill and two file links. Unknown skill text and absent/present context records must not be silently represented as successful resolved chips. The exact number or length of token labels is not a branch of this CSS rule; one long resolved label with adjacent arrows is enough for inherited-label/wrap coverage.

**H1 — labelled harness, not a real-app claim.** Mount the actual `ComposerPromptEditorTiptap` and actual `index.css` with bundled fonts using the existing browser-test renderer. Keep all harness files in the capturer's authorized scratch, not the PR. Use controlled `value`/`cursor`, `editorRef`, empty `contextRecords` map/`skills` unless testing tokens, `placeholder: "Ask anything..."`, an onChange handler updating value/cursor, and a no-op paste hook. Pass G's flags/values and change `disabled` visibly. Supplement D with one citation and one context reference using valid records from the existing citationDraft fixture, only if the app cannot supply them. Label these crops **HARNESS** and state exactly which production request/navigation path they do not establish. Never inject styling onto the captured editor or replace it with a hand-drawn equivalent.

**D sample.** Paste as Markdown, not HTML. Use separate paragraphs/blocks so the real parser constructs nodes. Include:

~~~~text
a -> b => c; office affine fi fl
**bold a -> b** *italic b => c* ~~strike a -> c~~
# Heading a -> b
> Quote b => c
- Bullet a -> b
1. Ordered b => c
- [ ] Task a -> b
---
[surface.test.ts](apps/web/src/terminal/ghostty/surface.test.ts) $frontend-design a -> b => c
`inline a -> b => c`
```ts
const f = (x: number) => x; // a -> b
```
~~~~

## Render-path and branch inventory / collapse rationale

- **CSS and Tiptap.** The only changed component surface is `.composer-tiptap.ProseMirror`. `ComposerPromptEditorTiptapInner` always assigns `composer-tiptap`; Tiptap supplies `ProseMirror`. Tiptap's installed `core/src/style.ts` disables both properties (including a WebKit alias). The added rule has two-class specificity and sits outside Tailwind layers. No provider, transport, server, model, count, length, theme, media-query, or platform predicate was added.
- **All production callers.** `ChatComposer` has one `<ComposerPromptEditor>`; `ChatView` has one `<ChatComposer>` under the `composerMounted` branch. `ThreadRouteView` calls `ChatView` for an existing draft, a draft-to-server handoff, or an existing/loading-known server thread. Missing drafts redirect; missing/unlisted-loading threads without a view do not expose an editor. Draft hero positioning and forced expanded mobile reuse it. Settings/Usage can keep that chat inert underneath their overlay. `PromptFontPreview` is the other direct editor caller, reached through `TypographySection` → simple Interface preview or Separate fonts Prompt preview. Test-only editor callers are harnesses, not extra product surfaces.
- **Content props.** `value` selects empty/nonempty/multiline/overflow; `cursor` selects active rich-marker decorations/caret/selection; `draftKey` remounts on draft switch. `richTextEnabled` and `literalText` choose rich/plain/literal document construction, with literal suppressing parsing. D/F/G cover these. `skills`, `contextRecords`, and context clipboard builders select skill/mention/citation/context-reference chips and context-only placeholder suppression, covered by D/S2/H1. Heading levels, nested list counts/marker widths, task checked state, bold/italic/strike, code, and blockquote all retain the same feature inheritance; D is a composite sentinel, not a demand for every list depth and heading level. Plain paragraphs and line breaks cover the unstyled branch.
- **Layout props.** `containerClassName`, `className`, and `placeholderClassName` can alter wrapper/editor/placeholder layout. Production differences are preview `max-h-42` versus chat `max-h-52`, and mobile pending-answer `max-sm:pb-12`. Root wrapper uses `--font-composer` falling back to `--font-sans`, prompt pixels, and a coarse-pointer minimum of 16px below `sm`; C/P/Q cover dimensions/families. Code uses `--font-mono` and code-size settings, covered by D/D2. No need to multiply font smoothing, interface sizes, color presets, theme preview overlays, and every custom font: record their fixed values and actual rendering faces. A provides both themes and widths; C stresses max prompt size and overflow.
- **Interaction/accessibility props.** `disabled` controls editability and aria-readonly; placeholder content/overlay is separate. `ariaLabel`, suggestion ids, active suggestion, and path-query callbacks control accessibility/keyboard behavior but do not select a new font style. `onChange`, selection callbacks, command/page-scroll hooks, paste/copy/cut, citation-submit/draft-save callbacks, and editor-ref focus/readSnapshot methods reach the same root; A/D/F/G exercise shaping-sensitive caret/selection/clipboard/remount operations. Specialized chip popovers/dialogs and code language menus are outside the changed root or retain their own control styles; their workflows are not changed by this rule. Contextual menus may affect available width/scroll but need no additional typography row.
- **Chat conditions.** Normal prompt vs custom answer, literal `initialAnswer`, choice-only readonly, responding readonly, approval/plan/refinement placeholders, missing project/provider, queued edit/restoration/history, busy/send/resume/error states do not choose a different root selector. G covers the alternate literal/readonly document; F covers restoration; X1/X2 collapse the surrounding states. Attachments (zero/one/many), previews, uploads and errors, token counts, banners, model pickers, multi-model controls, command/stash results (zero/loading/error/one/many), permissions, provider-specific footer controls are sibling layout, not new feature-setting branches.
- **Mobile web collapse.** `isMobileViewport && !forceExpandedOnMobile && !isComposerFocused && !hasMultilinePrompt && pendingUserInputs.length===0` hides the editor and paints a truncated button with `prompt.trim()` or placeholder. Focus, newline/wrap, a pending input, or draft hero forces expansion. A's short prompt records the switch; C's wrapped/overflowing prompt covers persistent expansion. Native iOS/Android do not use this web component; no mobile-native proof is required for this diff.
- **Platform and connection equivalence.** Desktop reuses the web UI/CSS, but Linux Electron is the reported client and gets A's one real-shell pair. Other providers, local/remote/tunnel servers, scheduled tasks and agent MCP entry points do not alter this client CSS; no separate rows are justified. Apple default faces precede Inter; Windows/browser font fallback and Firefox/WebKit shaping/caret behavior are not established by Linux Chromium. If broad platform/browser support is claimed, repeat A's Inter edit recording on each claimed engine/platform and use a default-font B pair there. Do not claim macOS default arrows must change. These are explicit limits of the minimum, not native-client coverage.

## Evidence acceptance

Source anchors at the reviewed head: [CSS rule](https://github.com/pingdotgg/t3code/blob/ab17d3b8b1295170213b7537dddcf00dadee7708/apps/web/src/index.css#L2423), [editor props and root](https://github.com/pingdotgg/t3code/blob/ab17d3b8b1295170213b7537dddcf00dadee7708/apps/web/src/components/ComposerPromptEditorTiptap.tsx#L1075), [chat caller](https://github.com/pingdotgg/t3code/blob/ab17d3b8b1295170213b7537dddcf00dadee7708/apps/web/src/components/chat/ChatComposer.tsx#L3311), [preview caller](https://github.com/pingdotgg/t3code/blob/ab17d3b8b1295170213b7537dddcf00dadee7708/apps/web/src/components/settings/SettingsFontPreviews.tsx#L30), [typography placements](https://github.com/pingdotgg/t3code/blob/ab17d3b8b1295170213b7537dddcf00dadee7708/apps/web/src/components/settings/TypographySettings.tsx#L291), [font inheritance](https://github.com/pingdotgg/t3code/blob/ab17d3b8b1295170213b7537dddcf00dadee7708/apps/web/src/appearanceFonts.ts#L75), [route callers](https://github.com/pingdotgg/t3code/blob/ab17d3b8b1295170213b7537dddcf00dadee7708/apps/web/src/components/ThreadRouteView.tsx#L108).

Retain per-side SHA, client/browser version, OS, viewport/DPR/zoom/pointer type, theme, font settings/sizes/smoothing, actual rendered font and weight, editor DOM/text node boundaries and computed properties. Record base `none`/`"liga" 0` versus head `normal`/`normal` where those are the actual computed results. Await font loading. Show the sent control and source text `->`/`=>` separately from their shaped glyphs. Confirm selection/copy/send leaves raw ASCII, rather than visually inferring it. Annotate intentional wrap/advance changes and any wider change; pixel differences need explanation, not an assumption that they are all noise. Keep cursor/focus/scroll and timestamps deterministic where possible without modifying production CSS. Full frames plus readable glyph crops are useful; magnified crops alone cannot prove layout.

Covered: complete exact diff, installed Tiptap style, both direct production callers and settings placements, route/caller chain, content/font/media/editability branches, issue, PR metadata and old evidence description. No captures, browser sessions, dev servers, tests, code edits, pushes, or forge comments were made. PR head and worktree were clean at inspection. Disk availability was 18 GiB, above the 5 GiB stop threshold.

FRICTION: none
