# PR #23373 evidence matrix (proof audit pass 1)

Before: main `8d3b061121` (`~/wt/pr-23373-before`), ThreadTerminalDrawer blob `0a12a6535c`.
After: head `7164868151` (`~/wt/pr-23373`), blob `9626c2640b`. Both verified with `git hash-object`.
Every capture's metadata must record `git rev-parse HEAD`, the full-tree `git rev-parse HEAD^{tree}`, the terminal blob, and a clean `git status --short`. Then the source binding is the full tree, not a restored component (this closes the earlier "historical" gap).

## What the diff changes (scope of proof)

`apps/web/src/components/ThreadTerminalDrawer.tsx` only:

- **Single terminal** (`!hasTerminalSidebar`, i.e. `normalizedTerminalIds.length === 1`): the floating `absolute right-2 top-2 z-20` action pill over terminal text becomes an in-flow row with `border-b`:
  - panel (`mode="panel"`): `h-[22px]`
  - drawer: `h-7 pt-1.5`, which puts the buttons below the drawer's 6px (`h-1.5`) resize strip (`absolute inset-x-0 top-0 z-20 cursor-row-resize`)
- **Two or more terminals** (`hasTerminalSidebar`): the sidebar header's buttons are now the shared `terminalActions` node, with no markup change. Layout is unchanged before and after.
- The empty state ("No terminal sessions…") is untouched.

Expected geometry: panel canvas top moves down 22 CSS px; drawer canvas top moves down 28 CSS px.

## Surfaces that exist (from code)

| Surface | Owner selector | How to reach | Notes |
|---|---|---|---|
| Bottom drawer | `[data-terminal-owner="drawer"]` (`data-thread-terminal-drawer`) | `button[aria-label="Toggle terminal drawer"]` in the thread header, or `mod+j` (`terminal.toggle`) | Height comes from the `height` prop, clamped to [180, min(0.75·viewport, column − header − composer)]. It has a resize strip and no maximize. **New** = `createNewTerminal`, which adds a new group to the sidebar. |
| Right-panel terminal | `[data-terminal-owner="right-panel"]` | `button[aria-label="Toggle right panel"]` (`mod+alt+b`), then the add-surface menu `role=button[name="Terminal T"]` (`setup.mjs`), or `mod+t` (`rightPanel.new`) with the panel open | `mode="panel"`, with exactly one group per surface (`PersistentThreadTerminals.tsx:398`). **New** = `addTerminalSurface` (`ChatView.tsx:3080`), which adds a new right-panel **tab**. No resize strip. At ≤980px (`RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY`) the panel is a sheet. |
| Right-panel maximize | `button[aria-label="Maximize panel"]` / `"Restore panel size"` (`PanelLayoutControls.tsx:144`) | Titlebar control, shown only when `showMaximizeControl` and the panel is open. Command `rightPanel.toggleMaximized` has no default key. | Panel only. `replay.mjs:102-104` already drives it. |
| Split panes | `${surface} button[aria-label^="Split Terminal Horizontally"]`, `^="Split Terminal Vertically"` | Toolbar buttons, `mod+d` / `mod+shift+d` (when `terminalFocus`) | Max 4 per group (`MAX_TERMINALS_PER_GROUP`, `types.ts:36`). At the limit the label gets "(max 4 per group)" and clicks are no-ops. Works in both drawer and panel. |
| New / Close | `button[aria-label^="New Terminal"]`, `^="Close Terminal"` | `mod+n` / `mod+w` (`terminalFocus`) | Close opens `[role="alertdialog"]` confirm (`confirmTerminalClose`). The sidebar rows also have per-terminal close. |
| Drawer resize strip | `${drawer} > .cursor-row-resize` | Pointer drag only (`handleResizePointer*`) | A drag ending at its start height restores the stored preference. Otherwise it persists through `onHeightChange` and bumps `resizeEpoch` (refit). |

Fixture helpers already on the branch (`apps/web/test/fixtures/terminal-layout/`):

- `setup.mjs`: fresh draft thread, opens RP/drawer, launches `shell.sh A`.
- `typeFirstLine`: runs `fixture_input`, which clears to row 1 with a visible cursor, then types `printf '%s\n' 'typed-xxx…RIGHT'`. The text is exactly `cols-2` wide, so it **does not wrap**.
- `replay.mjs`: hover, the top+2px `elementFromPoint` hit test (line 52), splits, the limit attempt, maximize/restore (102), and a strip drag to canvas (135).
- `shell.sh`: `LEFT-…-RIGHT` full-width prompt line; `fixture_scrollback`.
- The driver writes `ready-<pane>` milestones under `/tmp/t3-terminal-evidence`.

Rows below reuse these. New behaviour (the wrap, drag-up restore, sidebar top-edge probe) needs small additions, noted per row.

## Dropped / not applicable

- **Drawer maximize**: doesn't exist (no maximize prop or control on the drawer). Maximize is covered only for the right panel at W.
- **Right-panel maximize at N (390)**: the panel is a sheet at ≤980px. Include it only if `button[aria-label="Maximize panel"]:visible` exists at N; otherwise record "control absent at N" as a still and drop the row.
- **Right-panel resize strip**: not rendered in panel mode (`!isPanel` guard). The right panel's own width handle (`RightPanelResizeHandle`) is outside this diff. It's optional, and `replay.mjs:97` drags it.
- **Sidebar groups inside the right panel**: a panel surface always gets one group, and New makes a new right-panel tab instead. So "multiple groups" is drawer-only, and "multiple tabs" is panel-only.
- **Empty state**: unchanged by the diff, so no row.
- **Electron/macOS**: cannot verify on this Linux box (`cannot-verify desktop`).
- **Permission-unavailable** (Q1-N-L, Q2-W-D in manifest; `canOperateTerminal=false`, buttons disabled): optional. It needs a non-operate scope session and isn't part of the required coverage.

## Row conventions

- **Viewports:** W = 1440×900 CSS, N = 390×900 CSS, DPR 2, terminal font 12, drawer height 280, frozen clock `2026-10-21T22:30Z`.
- **Themes:** L = light, D = dark.
- **STILL PAIR:** a before and an after PNG from identical input bytes and equal terminal columns. Also the diff0 (threshold 0, gain 8) and diffT (threshold 8, gain 8) images. Plus a crop of the canvas's first text row (full width, ±4px).
- **RECORDING:** an mp4 with on-screen or timestamped narration per step, for both before and after. Each chapter starts from `setup()` on the stated SHA.

## Static rows (STILL PAIR)

| ID | Surface | VP | Theme | Steps | Pass (after) / expected (before) |
|---|---|---|---|---|---|
| S-P1-W-L | Right panel, single | W | L | `setup(mode=RP)`, then `typeFirstLine` | Whole surface plus first-row crop. After: actions sit in a 22px bordered row above the canvas. The canvas top is ≥ the row's bottom, and `printf…RIGHT` is fully legible, including its right end. Before: the pill overlaps the canvas's top-right. |
| S-P1-W-D | Right panel, single | W | D | Same as S-P1-W-L | Same as S-P1-W-L |
| S-P1-N-L | Right panel (sheet), single | N | L | Same as S-P1-W-L | Same as S-P1-W-L. At N, before must visibly hide the `RIGHT'` tail under the pill; after shows it. |
| S-P1-N-D | Right panel (sheet), single | N | D | Same as S-P1-N-L | Same as S-P1-N-L |
| S-D1-W-L | Drawer, single | W | L | `setup(mode=D)`, then `typeFirstLine` | After: the action row is `h-7` with buttons from y=6 (drawer top + 6) to y=28, and the canvas top is drawer top + 28. The first row is fully legible. Metadata box shows that the resize strip (0–6) doesn't intersect any button rect. |
| S-D1-W-D | Drawer, single | W | D | Same as S-D1-W-L | Same as S-D1-W-L |
| S-D1-N-L | Drawer, single | N | L | Same as S-D1-W-L | Same as S-D1-W-L, plus the `RIGHT'` tail is visible after and covered before. |
| S-D1-N-D | Drawer, single | N | D | Same as S-D1-N-L | Same as S-D1-N-L |
| S-WRAP-P-N-L | Right panel, single | N | L | `typeFirstLine`, then press Ctrl-U and type a string of `cols+20` chars, e.g. `printf '%s\n' 'wrap-…'` padded past `cols` (**new step**; the existing helper stops at `cols-2`) | Crop of rows 1–2: the wrapped input continues on row 2, and row 1 is fully visible after. Before: row 1's tail is covered. |
| S-WRAP-D-N-D | Drawer, single | N | D | Same wrap input as S-WRAP-P-N-L | Same as S-WRAP-P-N-L |
| S-WRAP-P-W-D | Right panel, single | W | D | Same wrap input, at W columns | Same as S-WRAP-P-N-L |
| S-WRAP-D-W-L | Drawer, single | W | L | Same wrap input, at W columns | Same as S-WRAP-P-N-L |
| S-SB-D-W-L | Drawer, 2 groups (sidebar) | W | L | `setup(D)`, then New (`button[aria-label^="New Terminal"]`), then wait for canvas | The sidebar header at drawer top is pixel-identical before and after (no change expected). It records the multi-terminal layout. |
| S-SB-D-W-D | Drawer, 2 groups (sidebar) | W | D | Same as S-SB-D-W-L | Same as S-SB-D-W-L |

The prompt line `LEFT-…-RIGHT` from `shell.sh` doubles as a "prompt as first line" check in every S row before `fixture_input` runs. Capture it as an extra crop if cheap.

## Interaction rows (RECORDING)

| ID | Surface | VP | Theme | Steps (narrate each) | Pass criterion visible in video / logged |
|---|---|---|---|---|---|
| R-TYPE-P-W-D | Right panel | W | D | 1. `setup(RP)`. 2. Click the canvas. 3. `typeFirstLine`. 4. Type 20 more chars to force a wrap. 5. Press Enter. 6. The output appears. | After: every typed char on row 1 stays visible as it's typed, including the last 8 columns. Before: the tail disappears under the pill. |
| R-TYPE-D-N-L | Drawer | N | L | Same as R-TYPE-P-W-D | Same as R-TYPE-P-W-D |
| R-HIT-D-W-D | Drawer, single | W | D | 1. `setup(D)`. 2. Hover each of the 4 actions to show the tooltip. 3. Log `elementFromPoint` at x = center, y = rect.top + 2 for each button (`replay.mjs:52` pattern, extended to all 4). 4. Real click at top+2 on Split H. 5. Verify the split happened. | After: the hit at top+2 is the button for all 4 (button top is drawer y ≥ 6), and the click splits (pane count 1→2). Before: the top+2 hit returns the button too, but the pill sits over the text. This row proves the strip doesn't steal the buttons after the change. |
| R-HIT-D-N-L | Drawer, single | N | L | Same as R-HIT-D-W-D | Same as R-HIT-D-W-D |
| R-HIT-SB-D-W-L | Drawer, sidebar (≥2 terminals) | W | L | 1. After a split, run the same top+2 probe on the sidebar header buttons. 2. Also probe at top+8. | Documents the **pre-existing** limit: at top+2 `elementFromPoint` hits the resize strip (drawer y=0–6 overlaps the `h-[22px]` header at y=0). At top+8 it hits the button. Expected identical before and after. This is a known gap the PR doesn't claim to fix. |
| R-RESIZE-D-W-L | Drawer, single | W | L | 1. `setup(D)` with the first-line text. 2. Record the drawer height (280). 3. Drag the strip from `${drawer} > .cursor-row-resize` up about 150px (target: a timeline element above). 4. Wait for the refit. 5. Drag back down to 280 (target the canvas or a coordinate). 6. Toggle the drawer closed and open (`mod+j`). | Height logs 280→~430 (clamped)→280 and persists across the toggle. After: the action row stays above the canvas at every height, and row 1 is legible after each refit. Before: the pill covers row 1 at each height. |
| R-RESIZE-D-N-D | Drawer, single | N | D | Same as R-RESIZE-D-W-L (the max clamp is smaller at N; log it) | Same as R-RESIZE-D-W-L |
| R-MAX-P-W-D | Right panel, single | W | D | 1. `setup(RP)` with the first-line text. 2. `Maximize panel`. 3. Crop row 1. 4. `Restore panel size`. 5. Crop row 1. | Both states: the action row is above the canvas and row 1 is legible. Restore returns to the original width (log the panel box). |
| R-SPLIT-D-W-L | Drawer | W | L | 1. Single terminal. 2. Split H. 3. Split V on the new pane. 4. Split until 4 panes. 5. A 5th attempt (label shows "max 4 per group" and the count stays 4). | After: the single-terminal row disappears once the sidebar appears (terminals ≥ 2). Each pane's row 1 is not covered by any control. Pane count stays at 4. |
| R-SPLIT-P-N-D | Right panel | N | D | Same as R-SPLIT-D-W-L | Same as R-SPLIT-D-W-L |
| R-GROUPS-D-W-D | Drawer | W | D | 1. Single terminal. 2. New (`mod+n` or button) to get group 2. 3. Click the group-1 row in the sidebar, then the group-2 row. 4. Close group 2 (button, then `[role=alertdialog]` confirm). 5. Back to single. | After returning to single, the 28px action row returns and row 1 is legible. The sidebar appears and disappears with no stale overlay. |
| R-TABS-P-W-L | Right panel | W | L | 1. Single panel terminal. 2. New (creates a 2nd right-panel tab). 3. Switch tabs via `[data-right-panel-tab-list]`. 4. Close the 2nd tab's terminal (confirm). 5. Return to tab 1. | Each tab shows the 22px row above its canvas with row 1 legible. After the close, the remaining tab is single and unchanged. |
| R-RETURN-D-N-L | Drawer | N | L | 1. Split to 3. 2. Close 2 (confirm each). 3. Single again. 4. `typeFirstLine`. | The single-terminal row returns at `h-7 pt-1.5`, and the first-line text is fully visible (closes the "New-close-return" gap). |
| R-RETURN-P-W-D | Right panel | W | D | Same as R-RETURN-D-N-L | The single-terminal row returns at `h-[22px]`, and the first-line text is fully visible. |

## Acceptance

- All 14 still pairs have the before/after plus diff0/diffT images and a first-row crop.
- All 14 recordings exist for both SHAs, with narration, and the metadata binds HEAD, the tree SHA, and the blob.
- Logged coordinates show the canvas-top shift of 22 (panel) and 28 (drawer).
- R-HIT-SB-D-W-L is expected to show no change, and its strip overlap should be reported as a pre-existing limitation, not a regression.
