# Composer nested ordered lists — proof matrix, pass 1

PR [#17138](https://git.cafe/pingdotgg/t3code/pull/17138), issue [#17124](https://git.cafe/pingdotgg/t3code/issues/17124).
Reviewed head [9b9c0c8831](https://github.com/pingdotgg/t3code/commit/9b9c0c8831d4002cae0d65f4ebd0039a9801843a), branch `michal-billtech:fix/composer-nested-ordered-list`, local `pr-17138`, worktree `/home/ubuntu/wt/pr-17138`.
Actual main base [becf853def](https://github.com/pingdotgg/t3code/commit/becf853defb6127dbe4447860383777d69346b6f), confirmed by origin/main, the merge parent, and GitCafe's revision-bound status. The local branch named main is older and is not the PR comparison base.

This is a source-derived capture plan. No browser, app, screenshot, video, or existing evidence artifact was inspected. Historical capture claims in the PR description do not count as proof for this pass. All rows await capture. No edits, pushes, or comments were made.

## Findings first

1. **P2 / fix before declaring the full parent-content-column behavior proven:** the backward scan returns at any non-marker line. With `10. parent\n    details\n11. child`, Tab produces `10. parent\n    details\n  11. child`, rather than a four-space `1. child` beneath the parent. This was confirmed by directly invoking the unmodified helper with Bun, not by inspecting pixels. It also means an empty item below continuation prose can exit instead of finding its parent. Row M must record this failure or the corrected behavior after a reviewed fix. Location: [listMarkersAbove, lines 188–189](https://github.com/pingdotgg/t3code/blob/9b9c0c8831d4002cae0d65f4ebd0039a9801843a/apps/web/src/composer-list-continuation.ts#L188). The PR has an older resolved comment about this condition; the current source still has it. Capture and any fix remain the lead's work.
2. **The two composer modes are real separate paths.** They share the Tab helper, but rich list Enter bypasses `listContinuationForEnter`: the handler returns false for a structural list item and the editor calls `splitOrLiftListItem`. Plain mode uses the changed continuation helper. Rich empty-item behavior is parity coverage, not evidence that the changed helper ran. The setting remounts the editor with a different key and extension set.
3. **The brief names an obsolete handler file.** At this head it is [composerCommandKey.ts](https://github.com/pingdotgg/t3code/blob/9b9c0c8831d4002cae0d65f4ebd0039a9801843a/apps/web/src/components/chat/composerCommandKey.ts#L175), not `useComposerCommandKey.ts`. The net diff is only the helper and its focused test file; no render component, CSS, native mobile code, or contracts changed.

## Render and interaction path inventory

- Production exports of the changed helper: `listIndentForTab` and `listContinuationForEnter` have one caller, `composerCommandKeyHandler`; `nextOrderedMarkerText` is unchanged and is also used by the rich document's list alignment/renumbering code. Tests are not extra product render paths.
- The handler is constructed by `ChatComposer` and passed to `ComposerPromptEditor` → `ComposerPromptEditorTiptap` → its inner editor. This is the only production `ComposerPromptEditor` call site and `ChatView` is the only production `ChatComposer` call site. `ThreadRouteView` reaches ChatView for a local draft, draft-to-server handoff, and an existing ready server thread. The same component is kept mounted inert behind Settings; this is not another editor implementation.
- Settings → Chat (`/settings/chat`) → **Rich text composer** writes `composerRichTextEnabled` using scoped-settings machinery. It is a client setting, defaults true, and reaches `ChatComposer` through merged settings. This is neither a build flag nor a harness-only prop. Inspect `data-composer-rich-text` after returning to the composer to confirm the effective mode. Source: [setting](https://github.com/pingdotgg/t3code/blob/9b9c0c8831d4002cae0d65f4ebd0039a9801843a/apps/web/src/components/settings/SettingsPanels.tsx#L1045), [editor mode/remount](https://github.com/pingdotgg/t3code/blob/9b9c0c8831d4002cae0d65f4ebd0039a9801843a/apps/web/src/components/ComposerPromptEditorTiptap.tsx#L788).
- Plain mode is still Tiptap, with literal Markdown paragraphs and inline chips. Rich mode enables structural ordered/bullet/task lists and styled content; list markers are not editable text positions. Both serialize to canonical Markdown and use the shared Tab handler. Rich list Enter is [split-or-lift](https://github.com/pingdotgg/t3code/blob/9b9c0c8831d4002cae0d65f4ebd0039a9801843a/apps/web/src/composer-rich-text-doc.ts#L1064), reached through [editor key handling](https://github.com/pingdotgg/t3code/blob/9b9c0c8831d4002cae0d65f4ebd0039a9801843a/apps/web/src/components/ComposerPromptEditorTiptap.tsx#L1280).
- Editing data dimensions touched by the diff: ordered `.`/`)`, number digit width/leading zeros, bullet versus checkbox markers, indent string and four-column tab stops, 1–4-column padding versus zero/>4 fallback padding, first child versus existing child, previous deeper descendants versus nearest sibling, equal/deeper/shallower preceding indent, matching/mismatching child kind/delimiter, blank/prose/BOF boundaries, empty/nonempty content, later siblings and their renumber boundaries, caret before replacement versus in content, and multiline inline-token boundaries.
- Key-routing conditions retained outside the diff: send shortcut and draft/running submission intents take priority; active slash/mention menu consumes Tab/Enter; Shift+Tab retains plan-mode/citation navigation behavior; IME composition, noncollapsed selections, unavailable selection, or a mismatched editor snapshot do not invoke a list edit. Literal pending answers opt out. Rich fenced code handles Tab/Enter as code. Enter before marker padding or inside an inline token returns no helper edit. These are guard coverage, not new loading/error UI.
- Sent path: ordinary composer send → canonical message text → MessagesTimeline → UserMessageRow → CollapsibleUserMessageBody → UserMessageBody → [ChatMarkdown](https://github.com/pingdotgg/t3code/blob/9b9c0c8831d4002cae0d65f4ebd0039a9801843a/apps/web/src/components/chat/TimelineUserMessageRow.tsx#L817). Short fixtures stay expanded; a diagram-extracted body also uses UserMessageBody. `ChatMarkdown` has an ordered-list gutter based on start and item count, and renders actual `ol`/`li` nesting. Verify DOM hierarchy, not only indentation: its nested marker style may differ from the stored `1.`. No renderer is changed here.
- Web (locally served or hosted/remote) and Electron desktop share this code. There is no OS or transport branch in either changed function. Native iOS/Android use `ComposerEditor` → native `T3ComposerEditor`; they do not import this helper or handler. Their sent-message renderer is also separate (`SelectableMarkdownText`). A narrow web viewport is not the native mobile client. Native editing proof is out of scope for this diff.
- No helper branch depends on theme, viewport, loading, error, provider, account count, or time. Message length can activate the existing collapse control and viewport width can wrap text; these short fixtures avoid both. Disabled/unmounted/inert composer states cannot exercise the edit. Do not invent screenshots of unrelated loading/error states.

## Minimum capture set and procedure

**One viewport × theme: 1440×900 CSS px, light, normal zoom; both real plain and rich modes.** Record the same seed/action chapters before and after. Four chaptered recordings total (before/plain, after/plain, before/rich, after/rich) can cover the real-app matrix; separate clips per row are optional. No dark/narrow Cartesian product is required: this diff changes Markdown edits, with identical relevant layout branches across those combinations. Include the entire composer and sufficient timeline for the sent result. Supplement the recordings with the A/B sent endpoint stills if useful; stills alone never prove these interactions.

Use the main base above for Before and the reviewed head for After. If a fix lands, record its exact head and regenerate only affected chapters; preserve the renderer and setting paths. Validate each starting Markdown and selection after paste, especially in rich mode; do not silently normalize a seed into a different case.

**Real-app seed S:** in the lead's isolated dev environment, choose/create a disposable stopped thread and navigate to `/<environmentId>/<threadId>`; a new draft is also reachable through the app's New thread action. Use `/settings/chat` to set the mode and default **Send shortcut = Enter**, then return, focus `data-testid=composer-editor`, select all, and paste the seed as text into the empty composer. `\n` below means a real newline and `\t` a real tab. `¦` marks the initial caret and is not part of the data. For rows without `¦`, use the end of the final item. Paste while empty so rich mode uses its ordinary Markdown import. Close suggestion menus before list actions unless testing menu priority. Use actual keyboard events for the operation, and type `X` at the resulting caret to make its position visible; delete only X before continuing a lift sequence.

**Sent seed R:** A and B must include the edited text becoming a real timeline user message, with the canonical submitted text retained as evidence. Prefer ordinary Submit in a disposable isolated thread with the lead's controlled provider fixture. If no controlled provider is available, the real guarded orchestration RPC supports `message.dispatch` with `dispatchMode: {type: "defer_start"}` (commandId, threadId, messageId, text, attachments, creation fields as required); use the exact editor snapshot, never a hand-fixed expected string. Label that chapter **RPC-seeded real renderer; Submit path not exercised**. It proves message rendering but is not ordinary-send proof. No database injection or renderer replacement is acceptable. Schema: [message.dispatch](https://github.com/pingdotgg/t3code/blob/9b9c0c8831d4002cae0d65f4ebd0039a9801843a/packages/contracts/src/orchestrationV2.ts#L2992).

**Harness H:** impossible rich prefix/inline-chip-internal caret offsets and stale snapshots must be labelled harness captures, never represented as real reachable caret positions. A small driver can call the actual helper/handler and display input, edit and caret; no product edit is needed. Real browser recording is still required for the reachable counterpart. No seeded route, driver, server, or browser has been launched in this pass.

## Fixture matrix

Rows group checkpoints with the same visual purpose; every listed variant must be shown within that row's recording. P = plain, R = rich. Every real row uses S at the minimum viewport/theme above. “Changed” refers to canonical edit behavior; rich Enter can be parity only.

| ID / named fixture | Exact data and actions | Expected checkpoints / affected branch | Seed / modes / medium | Status |
| --- | --- | --- | --- | --- |
| A — first ordered child, normal marker | Begin empty; type `1. parent`, Shift+Enter, Tab, type `child`. Also seed `1. parent\n2. ch¦ild`, Tab then X. Send the resulting unmodified nested draft. | Three spaces + `1. child`; caret stays between h/i (`chXild`). The generated empty child also starts at 1. Sent child is an `ol` inside the parent's `li`, not a second root sibling. Changed. | S + R; P/R; **recording**, sent endpoint still optional | Pending capture |
| B — wider parent / shorter replacement number | `10. parent\n11. ch¦ild`; Tab, X; send. | Four spaces + `1. chXild`; digit shrink preserves text caret. Sent parent starts at 10 and child is truly nested. Changed. | S + R; P/R; **recording** | Pending capture |
| C — existing sibling through deeper descendants | `1. parent\n   1. first\n      1. grandchild\n3. second`; Tab, X. | Reuse three-space sibling indent, produce child `2. secondX`; do not use the grandchild as the numbering source or modify earlier lines. Changed. | S; P/R; **recording** | Pending capture |
| D — padded paren continuation / list mismatch | Seed `1) parent\n   09) first\n      1) grandchild\n2) second`; Tab. Repeat with final `2. second`; repeat with prior child `   - first` and final `2. second`. | Matching `)` continues to `10)` at existing indent. Mismatched delimiter or nonordered previous child starts `1.` at the existing child indent. X proves caret in each result. Changed. | S; P/R; **recording** | Pending capture |
| E — content padding and tab stops | Independently seed `1.\tparent\n2. child`, `1.   parent\n2. child`, `1.     parent\n2. child`; Tab + X. | Respectively four, five, three inserted spaces before `1. childX`; >4 padding takes the one-column padding fallback. Keep original parent bytes. These are helper alignment cases; do not infer all unusual parent layouts from A's sent proof. Changed. | S; P/R, verify rich serialized seed; **recording** | Pending capture |
| F — ordered child of bullet / task parent | Independently `- parent\n2. child`, `- [x] parent\n2. child`; Tab + X. | Two spaces + `1. childX`; checkbox content starts after `- `, not after `[x] `. Rich checkbox is visibly distinct. Number reset changed; ordinary two-space bullet geometry retained. | S; P/R; **recording** | Pending capture |
| G — bullet and task children of ordered parent | Independently `1. parent\n- child`, `1. parent\n- [x] child`; Tab + X. | Three-space content-column indent; preserve bullet/check state and content, do not replace it with an ordered number. Rich task versus bullet are distinct checkpoints. Changed wider-parent indentation branch; ordinary bullet/task two-space nesting remains a control. | S; P/R; **recording** | Pending capture |
| H — empty grandchild lifts then exits | `1. parent\n   1. child\n      1. ¦`; Shift+Enter three times, X/delete-X after each. | First `   2. `, then top-level `2. `, then marker-free blank line; caret follows each surviving prefix. Changed in P; R structural-lift parity. Whitespace-only content is equivalent; keep a trailing-space variant as a helper check. | S; P/R; **recording** | Pending capture |
| I — middle lift renumbers later parent siblings | `1. a\n   1. ¦\n2. b\n   1. descendant\n03. c\n\n9. untouched`; Shift+Enter + X. | `1. a\n2. X\n3. b\n   1. descendant\n04. c\n\n9. untouched`; later same-level items renumber, descendants/beyond-boundary bytes do not. Also check a parent list using `)`; delimiter switch/prose/shallower indent stop renumbering. Changed in P; R parity. | S; P/R; **recording** | Pending capture |
| J — empty item lifts into nonordered parent | Independently `- parent\n  1. ¦` and `- [x] parent\n  1. ¦`; Shift+Enter + X/delete-X, then Shift+Enter again. | First lift yields outer `- X` or unchecked `- [ ] X`; next empty press leaves the list. No ordered-following-sibling renumber pass for these parents. Changed in P; R parity; rich checkbox separately visible. | S; P/R; **recording** | Pending capture |
| K — multiline mention is one atom | `1. @"parent\n999. fake"\n2. child`; Tab + X. Then `1. @"parent\n999. fake"\n   1. ¦`; Shift+Enter + X. | The fake marker inside the chip is never treated as the parent/sibling. Tab produces three-space child `1.`; lift produces outer `2. `. Chip source stays intact. Add a second earlier mention to exercise scanning past token ranges. Changed scan. | S; P/R; **recording**. Inline-chip-internal caret itself is H only. | Pending capture |
| L — prefix caret and no-parent fallbacks | P: `10. parent\n¦11. child`, Tab + X. R: same source, caret at start of visible child content. Then independent seeds `2. child`, `1. parent\n\n2. child`, `1. parent\n   1. child` (last line Tab). | P prefix caret lands after four indent spaces, before replacement `1.`; R cannot enter marker text and must retain its content caret. BOF/blank boundary/shallower-only parent use existing two-space fallback without resetting number. Reachable states are controls except the parent-found prefix mapping. | S; P/R **recording**; exact rich prefix offset H labelled **harness recording** | Pending capture |
| M — parent continuation line (known contradiction) | `10. parent\n    details\n11. ch¦ild`; Tab + X. Also `1. parent\n   details\n   1. ¦`; Shift+Enter. | Required: find actual parent through continuation prose, nest at four columns/reset number, or lift one level for the empty case. Current helper instead inserts two spaces/keeps `11.` in first case and removes the nested marker outright in second. Record failure; do not accept current bytes as the spec. P is directly reachable; R's structural parsing/normalization must be documented. | S; P required, R reachable counterpart; **recording** | Pending capture; contradicted by helper probe |
| N — unchanged key precedence and rejection gates | Active mention menu + Tab; selected text across list content + Tab; Shift+Tab; rich fenced-code Tab; literal pending-answer Enter; empty/non-list text. Separately drive composing event, missing selection, stale snapshot, invalid offsets, inline-token-internal offset, and Enter before marker end/without marker space. | List edit must not steal menu selection, plan shortcut, code indentation, literal answers, composition, selection, or stale editor state. Normal nonempty Shift+Enter continues/splits and renumbers as before. No new UI branch. Use one representative real checkpoint per reachable gate; display helper/handler rejection for unreachable states. | S for menu/selection/Shift+Tab/code/non-list; literal-answer only if a real pending-input fixture is available, otherwise H. Others H labelled **harness recording**; P/R as applicable. | Pending capture |

The matrix scales to a keyboard helper: 14 chapter groups, one viewport/theme, two modes, no provider/theme/platform grid. It includes the new mixed-marker branches because the helper changed them even though the headline bug is ordered lists. Repeated related checkpoints share a chapter rather than requiring separate files. Each relevant helper outcome and each distinct composer path is named; unrelated component props are deliberately held constant.

## Pass-1 validation and limits

Read the full two-file diff against the actual main base; traced all helper production callers, settings, editor extension selection, rich Enter, canonical replacement/caret path, sent Markdown renderer, desktop sharing and native mobile exclusion. Direct helper probes confirmed A/B/C-family numbering, tab padding, wide-padding fallback, task-parent content column, multiline chip scan, parent renumbering and M's contradiction. No test suite or real client was run, and no visual proof is claimed. Worktree remained clean. Disk had 22 GB free, above the 5 GB stop threshold.

FRICTION: brief | The supplied useComposerCommandKey.ts name no longer exists at this head; the implementation moved to composerCommandKey.ts. | Supply the current path, or specify the behavior and let source tracing locate it.

VERDICT: INSUFFICIENT all rows pending capture
