docs(coder): inventory the coder-lite TUI against the TS and Rust coder UIs

3a3f31e8af23 · AtlantisPleb · · parent dc06db01cfbf

docs(coder): inventory the coder-lite TUI against the TS and Rust coder UIs

The deliverable the porting half of #117 is gated on. 79 feature rows across
three live implementations — coder-lite, `oa coder`, and `coder-ui.ts` — each
with exactly one disposition and a file, line, or count behind it (V3).

Counts: 32 ported, 10 dropped with reasons, 31 to-port, 2 flagged as boundary
violations as scoped, 4 recorded as undetermined rather than guessed.

Five of the issue's premises did not survive contact and are corrected in §2:
the TypeScript composer has no cursor variable, no history, and no completion,
so coder-lite already wins composer parity rather than owing it; the
"fixed-row template" line describes `coder-ui.ts`, not `tui.rs`; coder-lite
does have scrollback; `packages/input-bindings` has zero consumers and is
DOM-shaped; and `--offline`, which #116 assumes, does not exist in
coder-lite's parser — which makes it item 1 of the ordered list.

Written against the post-0.0.2 shape: one shipped artifact answering two
fronts, so the runtime boundary is now a module seam held by review rather
than a binary seam held by a linker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K7q2vA5LJroLTR6ZFbRq6j
Co-Authored-By
Claude Fable 5 <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • added docs/coder/2026-08-26-coder-lite-tui-inventory.md

Diff

1 file changed, +667 -0

docs/coder/2026-08-26-coder-lite-tui-inventory.md added +667

@@ -0,0 +1,667 @@

1
# coder-lite TUI parity inventory
2
3
The inventory issue #117 gates its porting half on. Every feature below carries
4
exactly one disposition — **ported**, **dropped**, or **to-port** — and every
5
claim carries a file, a line, or a count. Best practice V3: parity claims
6
quantify, and adjectives are not evidence.
7
8
Read `docs/coder/autoimprove.md` §4 and `docs/coder/best-practices.md` V2/V3
9
first. The porting half of #117 remains blocked on #116 (the PTY harness): no
10
entry here may be marked done on headless evidence.
11
12
---
13
14
## 0. Two notes on freshness
15
16
**Tree state.** Everything below was read at `f3bad5277b` (2026-08-26). Line
17
numbers are from that tree.
18
19
**Release 0.0.2 changes the framing, not the boundary.** As of release 0.0.2
20
the shipped artifact *is* `coder-lite`, and it answers two fronts:
21
22
- bare invocation with no arguments → the interactive TUI session
23
- `<binary> issue list` and peers → dispatches into the `openagents-cli`
24
  command set
25
26
So the module doc in `crates/coder-lite/src/main.rs` that currently says this
27
binary "is not `openagents`", and the framing of `oa coder` as a separately
28
shipped second front end, is **stale on arrival**. This document is written
29
against the post-release shape: one artifact, two fronts. Where a count below
30
comes from the pre-release tree it says so.
31
32
The consequence for #117's guardrail is that the runtime boundary gets
33
**sharper, not softer**. It is no longer a binary seam that a linker enforces;
34
it is a module seam inside one shipped executable, held only by review. Tools,
35
refusal patterns, composer semantics, lanes, the model catalog, plugins,
36
skills, goals, and metering stay in `openagents-cli`. `coder-lite` owns the
37
frame, the palette, the spinner, the system prompt, and the session loop. A
38
"to-port" that would move runtime logic across that seam is a boundary
39
violation and is flagged as one in §7 rather than scheduled.
40
41
`--version` reports the release version after 0.0.2, not `CARGO_PKG_VERSION` as
42
`main.rs:129` does today.
43
44
---
45
46
## 1. What is actually being compared
47
48
The issue says "the TypeScript and Rust coder UIs". There are **three**
49
implementations, not two, and all three are live.
50
51
| # | Implementation | Entry | Size | Status |
52
|---|---|---|---|---|
53
| 1 | **coder-lite** (Rust) | bare `coder-lite` → `interactive::run_tui` | 3,976 app lines + 24,127 vendored markdown | the shipped UI (#105) |
54
| 2 | **`oa coder`** (Rust) | `cli.rs:1446` → `interactive::run_tui` | 5,176 TUI-stack lines + 2,017 shared composer | live, second front |
55
| 3 | **`coder-ui.ts`** (TypeScript) | `cli.ts:57` `runCoderUi` | 3,862 lines | live in the npm CLI |
56
57
Per-file sizes:
58
59
- **coder-lite**: `markdown/` 26,113 (of which the port is 24,127 `.rs`),
60
  `interactive.rs` 581, `tui.rs` 504, `runtime.rs` 503, `transcript.rs` 485,
61
  `commands.rs` 419, `acp_tool.rs` 416, `acp.rs` 310, `export.rs` 276,
62
  `main.rs` 213, `osc8.rs` 191, `acp_harness.rs` 60, `lib.rs` 18.
63
- **`oa coder`**: `interactive.rs` 1,370, `markdown.rs` 1,120, `diff.rs` 1,092,
64
  `tui.rs` 841, `pty.rs` 753.
65
- **shared composer** (`openagents-cli/src/composer/`): `edit.rs` 650,
66
  `mod.rs` 463, `complete.rs` 324, `history.rs` 308, `keys.rs` 272.
67
- **TypeScript**: `coder-ui.ts` 1,767, `coder-session.ts` 1,415,
68
  `coder-markdown.ts` 490, `coder-plain.ts` 190.
69
70
Test weight, since the whole point of #116 is that none of it observes a real
71
terminal:
72
73
| Surface | Tests | What drives them |
74
|---|---|---|
75
| coder-lite | 674 (596 in `markdown/`, 49 integration, 29 unit) | `TestBackend` buffers; **15** are frame tests (`tests/frame.rs` 8, `tests/rebase_contract.rs` 7) |
76
| `oa coder` | 189 over the TUI stack (170 `#[test]`, 19 `#[tokio::test]`) | `TestBackend` for display; **real** PTYs, child processes, git repos, and SSE for the rest |
77
| TypeScript | 150 cases (`test/coder-ui.test.ts`, 1,353 lines, 14 `describe`) | fake stdin/stdout, string assertions |
78
79
**Nothing anywhere drives a real terminal.** `oa coder` gets closest — it runs
80
real child processes under real pseudoterminals and asserts a genuine
81
`SIGWINCH` round-trip (`tests/coder_tui_test.rs:1934`) — but even there the
82
display side is a `TestBackend`. That is exactly the gap #116 fills, and
83
`openagents-cli` already depends on `portable-pty` (`pty.rs`), so the harness
84
has its dependency in the workspace already.
85
86
---
87
88
## 2. Where the issue's premises did not survive contact
89
90
Recorded per the honesty contract. Three of the six existing CC teardowns had
91
to do the same and were better for it.
92
93
**2.1 — "composer multi-line editing, prompt history recall, kill/yank" are
94
not things to port *from* TypeScript. coder-lite already wins all three.**
95
96
The TypeScript composer is **one flat string** — `let composer = ""` at
97
`coder-ui.ts:485`, with **no cursor index variable anywhere in the file**. Text
98
is only appended (`:1732`) or truncated from the end (`:1702`, `:1709`).
99
Verified absent in `coder-ui.ts`: keyboard newline insertion (`\r`/`\n` are
100
unconditionally submit, `:1626-1653`), any intra-composer cursor movement,
101
word-wise editing, `ctrl+w`/`ctrl+k`/`ctrl+y`/`ctrl+a`/`ctrl+e`, forward
102
delete, prompt history of any kind (no `history` identifier in the file, no
103
on-disk file), and Tab completion of any kind (`\t` is bound to
104
`session.cycleReasoning()` at `:1694`).
105
106
coder-lite has all of it, through the grok-derived composer: 22 classified
107
chords in `composer/keys.rs:38-183`, newline on Alt+Enter / Shift+Enter /
108
Ctrl+J (`composer/mod.rs:115-125`), a 500-entry history persisted to
109
`~/.openagents/coder-history` (`composer/history.rs:21`, `:46`), and Tab
110
completion over commands, paths, and `@`-mentions (`composer/complete.rs`, 15
111
tests).
112
113
The direction of that arrow is the opposite of what the issue assumed.
114
115
**2.2 — "coder-lite has a fixed-row template with constants for
116
fleet/composer/spacer rows" describes `coder-ui.ts`, not `tui.rs`.**
117
118
The phrasing comes from `docs/teardowns/cc/04-terminal-ui-components-theme.md`,
119
where it correctly describes the **TypeScript** layout: `STATUS_ROWS = 1`,
120
`COMPOSER_ROWS = 3`, `SPACER_ROWS = 1`, `FLEET_ROWS_MAX = 8`,
121
`PREVIEW_ROWS = 3`, `SIDEBAR_WIDTH = 34`, `CHILD_OUTPUT_ROWS = 12`,
122
`GUTTER = 4`, `ESCAPE_WINDOW_MS = 40` (`coder-ui.ts:107-176`).
123
124
`tui.rs` uses a ratatui `Layout` with `[Min(0), Length(input_box_height),
125
Length(1)]` (`tui.rs:273-280`) where the composer grows 1..8 rows with its
126
content (`tui.rs:268-271`). It has no fleet rows and no spacer row because it
127
has no fleet display at all.
128
129
**2.3 — coder-lite is not missing scrollback.** `tui.rs:170-173` carries
130
`scroll_override: Option<u16>` / `scroll_max` / `transcript_height`;
131
`effective_scroll` (`:370-376`) follows the bottom on `None` and clamps an
132
explicit offset; `scroll_by` (`:381-390`) releases the override at the bottom
133
so the viewport resumes following. PageUp/PageDown move one viewport
134
(`interactive.rs:465-472`); Up/Down scroll one line once the prompt history is
135
exhausted (`:457-464`). `entries` is an unbounded `Vec` with no cap and no ring
136
— the same as both other implementations.
137
138
**2.4 — `packages/input-bindings` is not a source for this work.** It is 1,125
139
lines with **zero runtime consumers** (the only files that mention it are its
140
own `src/`, `package.json`, `README.md`, and one assurance-spec fixture). Its
141
binding shapes are DOM-native — `KeyboardEvent.code`, `KeyboardEvent.key`,
142
mouse buttons, wheel deltas (`src/index.ts:52-79`) — and its README says it was
143
written for "Autopilot Desktop and Verse", an app that was deleted on
144
2026-08-04 (#9325). It has a `"terminal"` context literal (`:19`) and nothing
145
that consumes it. A terminal binding table cannot be built from
146
`KeyboardEvent.code`. Recorded as **dropped (not applicable)**; coder-lite's
147
binding surface is `commands.rs:KEYS` plus `composer/keys.rs`.
148
149
**2.5 — `--offline` does not exist in coder-lite.** Its argument parser
150
(`main.rs:105-150`) accepts exactly five flags: `-h`/`--help`, `-V`/`--version`,
151
`--dev`, `--lane`, `--reasoning`; anything else is refused by name. Issue #116
152
names `--offline` in its minimum assertions ("use `--offline`, so the harness
153
needs no provider credential and no network"). The flag exists only on the
154
other front (`cli.rs:646`, `run_offline_coder` at `cli.rs:1411`). **This blocks
155
#116**, and it is item 1 of §6 for that reason.
156
157
**2.6 — "streaming markdown is already ported" is true, and the degree is
158
99.8%.** See §3.C.
159
160
---
161
162
## 3. The feature matrix
163
164
Legend: **P** ported · **D** dropped · **T** to-port. `CL` = coder-lite,
165
`OA` = `oa coder`, `TS` = `coder-ui.ts`.
166
167
### A. Composer and input
168
169
| # | Feature | CL | OA | TS | Disposition |
170
|---|---|---|---|---|---|
171
| A1 | Multi-line edit (Alt+Enter / Shift+Enter / Ctrl+J newline) | yes | yes | no | **P** — `composer/mod.rs:115-125` |
172
| A2 | Cursor movement: grapheme, word, logical line | yes | yes | no | **P** — 22 chords, `composer/keys.rs:38-183` |
173
| A3 | Kill: Ctrl+U to line start, Ctrl+K to line end, Ctrl+W word | yes | yes | partial | **P** — TS has only Ctrl+U, and it clears the whole composer (`coder-ui.ts:1708`) |
174
| A4 | Forward delete (`Delete`, Alt+D) | yes | yes | no | **P** — `composer/keys.rs:69`, `:155` |
175
| A5 | Prompt history, 500 entries, persisted to disk | yes | yes | no | **P** — `composer/history.rs:21`, `:46`, `:54` |
176
| A6 | Tab completion: commands, paths, `@`-mentions | yes | yes | no | **P** — `composer/complete.rs`, 15 tests |
177
| A7 | Kitty keyboard disambiguation | yes | no | yes | **P** — `interactive.rs:133-136` (3 flags); TS uses `\x1b[>1u` |
178
| A8 | Caret drawn where the caret is, plus a block cursor | yes | yes | n/a | **P** — `tui.rs:334-361`; TS has no caret to draw |
179
| A9 | **Bracketed paste** | no | no | yes | **T** — §6.3 |
180
| A10 | **Paste placeholdering** (`[Pasted text #1 +10 lines]`) | no | no | yes | **T** — `coder-paste.ts:15-44`, thresholds 800 chars / 1 line |
181
| A11 | **Image paste** (dropped paths to `[Image #N]`) | no | no | yes | **T** — `coder-image.ts`, 4 extension families; boundary-split, §7 |
182
| A12 | Kill-ring / yank (Ctrl+Y) | no | no | no | **T (low)** — absent everywhere; upstream has it at `xai-ratatui-textarea/src/textarea.rs:2452` |
183
| A13 | Undo / redo | no | no | no | **T (low)** — upstream `textarea.rs:2245`, `:2261` |
184
| A14 | Tier / reasoning cycling on Tab / Shift+Tab | no | no | yes | **D** — removed from the Rust line deliberately; the recorded reason is at `oa coder` `tui.rs:596-607` (a `Tab: effort` hint removed for being inert). Tab is completion in coder-lite and that is the better claim on the key |
185
186
The composer port itself: 922 of upstream `xai-ratatui-textarea`'s 12,237
187
`src/` lines were taken (`editor_keys.rs` 205 to `keys.rs` 272 with added docs
188
and 5 tests; `editor.rs` 1,002 to `edit.rs` 650, trimmed). `textarea.rs` (3,612
189
— the widget, with selection, undo, and the kill buffer) and `wrapping.rs`
190
(605) were not: the wrap geometry is written locally in `composer/mod.rs:235`
191
`wrap_rows`. A12 and A13 are what that omission costs.
192
193
### B. Keybindings
194
195
| # | Feature | CL | OA | TS | Disposition |
196
|---|---|---|---|---|---|
197
| B1 | Binding count | 7 session + 5 composer-level + 22 chords = **34** | **68** | **38** | — |
198
| B2 | Advertised-vs-handled invariant, commands | yes | yes | no | **P** — `interactive.rs:563-580` (two tests). TS's `/help` is a literal and has already drifted: it advertises 8 commands and omits `/plugin` (`coder-session.ts:940-948`) |
199
| B3 | Advertised-vs-handled invariant, **keys** | no | yes | no | **T (small)** — `oa coder` presses every hint the bar names (`tests/coder_tui_test.rs:294`, `:1992`). coder-lite's `KEYS` table (`commands.rs:49-60`) is asserted by nothing |
200
| B4 | Rebindable actions, contexts, chords, user config | no | no | no | **D** — no implementation has it; `packages/input-bindings` is not usable here (§2.4). Revisit only with owner direction |
201
| B5 | Esc / Ctrl+C / Ctrl+D / Ctrl+Q all exit | yes | partial | partial | **P** — `interactive.rs:325-337`, checked before the composer sees the key. Note: **no interrupt-a-running-turn key**; see E9 |
202
203
### C. Rendering — the vendored markdown engine
204
205
The port is **structural, not semantic**. Against the local grok-build
206
checkout:
207
208
| Measure | Value |
209
|---|---|
210
| Upstream `xai-grok-markdown/src` lines | 20,663 |
211
| Vendored lines from that crate | 20,699 (**99.8%** carried, 8 of 23 files byte-identical) |
212
| Harvested from `xai-grok-pager-render` | 3,145 (`core.rs`, `wrapping.rs`, `line_utils.rs`, `util.rs`) |
213
| Total vendored `.rs` | 24,127 |
214
| Semantic (not path-rewrite) changes | 3: `util.rs` narrowed 484 to 56 (2 of 20 functions kept), `line_utils.rs` dropped the `tool_paths` re-export, `streaming.rs` gained a `reparsed_bytes` counter |
215
| Tests carried | 596 in `markdown/` |
216
217
| # | Feature | Disposition |
218
|---|---|---|
219
| C1 | Streaming checkpoint / tail rewind, 8 checkpoint kinds | **P** — `markdown/checkpoint.rs:41-58`, `streaming.rs:335-350` |
220
| C2 | Incremental open-code-block highlighting | **P** — `open_code_highlighter.rs`, 439 lines |
221
| C3 | Syntax highlighting, full `two-face` extended grammar set | **P** — `syntax.rs:41` |
222
| C4 | Tables with alignment, cell wrap, links in cells | **P** — `parse.rs:1045-1061` |
223
| C5 | Task lists, strikethrough (double-tilde) | **P** — `core.rs:23-25` |
224
| C6 | LaTeX to Unicode, 4 delimiter forms plus `\begin{equation}` | **P** — `latex/` 1,990 plus `latex_delimiters.rs` 1,305 |
225
| C7 | Mermaid to Unicode box art (`graph`/`flowchart`/`sequenceDiagram`/`stateDiagram`) | **P** — `mermaid.rs`, 5,237 lines byte-identical |
226
| C8 | CJK / emoji width-correct soft wrap | **P** — `wrapping.rs:14`, `:89`. TS has no wcwidth at all (`coder-markdown.ts:39-41` counts a CJK glyph as 1 column) |
227
| C9 | Hyperlink metadata plus wrapped-fragment grouping | **P** — `hyperlinks.rs` 784 |
228
| C10 | Plain-URL autodetection in prose | **P** — `url_scan.rs` |
229
| C11 | OSC-8 emission | **P** — own emitter, `osc8.rs` 191 lines. Upstream's lives in the pager; TS parses no markdown links at all |
230
| C12 | Wrap cache keyed `(width, generation)`, O(n^2) to O(n) | **P** — `transcript.rs`, observable via `WrapStats` |
231
| C13 | Mermaid SVG/PNG raster | **D** — upstream `xai-grok-mermaid` (2,170 lines: dagre layout, resvg raster, `mmdc`) not ported. A terminal cannot show raster without a graphics protocol coder-lite does not negotiate |
232
| C14 | Footnotes | **D** — absent upstream too (`core.rs:22-27` does not set `ENABLE_FOOTNOTES`; the `FootnoteReference` arm at `parse.rs:815` is unreachable). Faithful port of an upstream gap, not a porting loss |
233
| C15 | Criterion benches (603 lines), fuzz target plus 9 seed corpora, 3 playground binaries (877 lines) | **D** — 1,515 non-library lines, no product value here |
234
| C16 | Inline graphics (Kitty protocol, iTerm2 detection) | **D** — upstream keeps it in `xai-grok-pager-render/src/terminal/image.rs`, outside the markdown crate. Out of scope for #117 |
235
| C17 | **NO_COLOR / 256 / 16-colour degradation** | **T** — the machinery is ported (`colors.rs:53-95` `detect_color_level`, honours `NO_COLOR` at `:56`) and then **defeated**: `transcript.rs:277` calls `theme::amberize`, which overwrites every span's fg/bg with `Rgb(255,176,0)` / `Rgb(8,6,0)` (`theme.rs:112-123`). `tui.rs` hardcodes the same two RGB values for all chrome. So `NO_COLOR=1` changes nothing a reader sees. See §6.7 |
236
237
The engine is about 6x the size of the application consuming it: 24,127
238
vendored lines against 3,976 in the rest of coder-lite. Only three files touch
239
it — `transcript.rs:23-25`, `tui.rs:15` (palette constants only), and `mod.rs`.
240
241
### D. Rendering — the frame
242
243
| # | Feature | CL | OA | TS | Disposition |
244
|---|---|---|---|---|---|
245
| D1 | Transcript scrollback with follow-the-bottom | yes | yes | yes | **P** — §2.3 |
246
| D2 | Per-role styling | 6 roles | 5 roles | 5 roles | **P** — `tui.rs:22-38`. CL adds `Role::Output` for command output, so a `/diff` is markdown-rendered but exported as a notice rather than as a model step |
247
| D3 | Braille spinner while streaming | yes | pulse | pulse | **P** — `tui.rs:20`, 10 frames. OA and TS use a 2-phase glyph pulse (400 ms / 500 ms) |
248
| D4 | Tool-call header with per-tool titles | yes | no | yes | **P** — `runtime.rs:398-416`, 5 tools. **OA renders no tool calls at all**: `Role::Tool` exists (`tui.rs:74`) and nothing constructs it |
249
| D5 | `— failed` on the header of a failed call | yes | no | yes | **P** — `interactive.rs:384-386` |
250
| D6 | Differential painting | ratatui | ratatui | hand-rolled | **P** — ratatui diffs cells; TS diffs a `painted[]` row array (`coder-ui.ts:1202-1219`) |
251
| D7 | Resize handling | implicit | explicit | explicit | **P** — CL redraws from `f.area()` each loop iteration, so a resize is absorbed. Untested; #116 names it |
252
| D8 | **Tool-call collapse / expand** | no | no | yes | **T** — `coder-ui.ts:1371-1377`, `expanded: Set<callId>`, Ctrl+O, newest call only. CL is fixed at exactly 5 lines |
253
| D9 | **Tool status marks** (running / ok / failed) | no | no | yes | **T** — `coder-ui.ts:675-680`. CL shows a bare bullet and a text suffix on failure only |
254
| D10 | **Tool box pads to 5 rows even for 1 line of output** | bug | — | — | **T (fix)** — `tui.rs:454` `for i in 0..5` emits 5 rows unconditionally, so a one-line tool result costs four blank transcript rows |
255
| D11 | **Diff inspector pane** | no | yes | no | **T** — CL renders a static fenced `diff` block through the markdown engine (`commands.rs:154-191`); OA has a navigable pane: unified plus side-by-side (`v`), Tab file cycling, `CONTEXT = 3`, line-level Myers with `MAX_EDIT_DISTANCE = 1500`, 18 tests |
256
| D12 | Word-level / intra-line diff | no | no | no | **T (low)** — absent everywhere |
257
| D13 | Diff syntax highlighting | yes | no | n/a | **P** — CL's fence goes through syntect. **OA's dedicated pane has none** (`diff.rs` imports only `truncate_spans`). CL wins this one |
258
| D14 | **PTY pane** (`/run` under a pseudoterminal) | no | yes | no | **T** — CL's `/run` is a plain non-interactive `/bin/sh -c` into the 5-line box and says so (`commands.rs:213-219`). OA has `vt100::Parser` emulation, `TIOCSWINSZ`/SIGWINCH, Ctrl+] detach, a 19-arm key encoder, 20 tests |
259
| D15 | Transcript search / highlight | no | no | no | **T (low)** — CC has it; nothing here does |
260
| D16 | Splash / empty state | no | no | yes | **T (low)** — TS has a 3-tier wordmark with a deterministic texture (`coder-ui.ts:295-411`) |
261
| D17 | Box frame with badge title | composer only | yes | no | **T (owner-gated)** — OA draws `OpenAgents │ openagents coder` in a bordered header (`tui.rs:280-293`). #105 names CL's frame as the product, so this is a look change, not a port |
262
| D18 | Mouse support | no | no | no | **D** — TS states the reason (`coder-ui.ts:61-65`): alternate scroll (`?1007h`) was chosen instead so the terminal's own text selection keeps working. Adopt that reasoning |
263
264
### E. Fleet, delegation, and status
265
266
| # | Feature | CL | OA | TS | Disposition |
267
|---|---|---|---|---|---|
268
| E1 | **Live child rows during `/delegate` fan-out** | no | no | yes | **T (highest)** — §6.2 |
269
| E2 | **Child full-screen transcript view** | no | no | yes | **T** — `coder-ui.ts:915-980`, 5 entry kinds, `CHILD_OUTPUT_ROWS = 12` |
270
| E3 | **Stop the fleet** (Ctrl+X) | no | no | yes | **T** — `coder-ui.ts:1685-1690` |
271
| E4 | Token usage in the status bar | yes | yes | no | **P** — `tui.rs:342-352`. **TS never renders it**: `CoderMetrics` exists on every entry (`coder-session.ts:173-179`) and `coder-ui.ts` reads it zero times |
272
| E5 | **Model in the status bar** | no | yes | yes | **T** — `ui.model` is set from `Control::Model` (`interactive.rs:397`) and read only by `/export` (`:531`). Never rendered |
273
| E6 | **Lane in the status bar** | no | yes | no | **T** — CL prints the lane once in the opening notice (`interactive.rs:110`) and never again |
274
| E7 | **Repo / branch in the status bar** | no | no | yes | **T** — CL sets both from `git_info()` (`interactive.rs:71-72`) and reads them only in `/export` |
275
| E8 | **Reasoning level surfaced** | no | no | yes | **T** — `ui.reasoning` is assigned at `interactive.rs:73` and **read nowhere in the crate**. A dead field |
276
| E9 | **Interrupt a running turn** | no | no | yes | **T** — TS `esc`/`ctrl+c` interrupt, then exit when there is nothing to interrupt (`coder-ui.ts:1347-1369`). In CL every exit key leaves the session (`interactive.rs:325-337`); there is no way to stop a turn without leaving |
277
| E10 | **Reasoning summaries in the transcript** | no | no | yes | **T** — `Role::Reasoning` has styling (`tui.rs:477`) and is constructed **only by a test** (`tests/rebase_contract.rs:64`). OA accumulates reasoning in `runtime.rs` and never surfaces it |
278
| E11 | Elapsed time on the running turn | no | no | yes | **T (small)** — `working… (2m 5s)` (`coder-ui.ts:1109-1111`) |
279
| E12 | Scroll-position indicator | no | no | yes | **T (small)** — `coder-ui.ts:1112-1114` |
280
| E13 | Home / End to top and bottom of transcript | no | diff pane only | yes | **T (small)** — `coder-ui.ts:1612-1613` |
281
| E14 | Status state word (running / streaming / ready) | no | yes | no | **T (small)** — `oa coder` `tui.rs:655-674` |
282
| E15 | Key hints in the status bar, dropped whole when narrow | no | yes | partial | **T (small)** — OA has 14 hints across 5 pane-aware lists (`tui.rs:608-624`) and drops them rather than showing half of one (`tests/coder_tui_test.rs:461`) |
283
| E16 | Thread id | no | no | no | **T** — named by #117; no implementation shows it. See §8 |
284
| E17 | Cost, context-window percentage | no | no | no | **D** — out of scope for #117; neither fact is computed anywhere in the three surfaces |
285
| E18 | `ui.agents` (discovered ACP agents) rendered | no | n/a | n/a | **T (small)** — assigned at `interactive.rs:91`, read nowhere. Announced once in the opening notice text instead |
286
287
### F. Commands
288
289
coder-lite handles **7** (`commands.rs:24-44`): `/clear`, `/diff`, `/export`,
290
`/help`, `/login`, `/resume`, `/run`.
291
`oa coder` handles **6** (`interactive.rs:63-79`): the same minus `/login`.
292
TypeScript handles **10**: `/reload`, `/skills`, `/plugin load`, `/resume`,
293
`/delegate` (UI side) and `/system`, `/goal`, `/export`, `/help`, `/?`
294
(session side).
295
296
| # | Command | Disposition |
297
|---|---|---|
298
| F1 | `/clear`, `/diff`, `/export`, `/help`, `/resume`, `/run` | **P** |
299
| F2 | `/login` — device flow driven from inside the TUI | **P** — coder-lite only (`commands.rs:106-116`); neither other surface has it |
300
| F3 | `/delegate` as a typed command | **T** — TS `parseDelegateCommand` accepts `[<n>x] <prompt>` (`coder-delegate.ts:96`). CL can only delegate through the model tool |
301
| F4 | `/skills` screen | **T** — TS has an arrows plus space-toggle screen (`coder-ui.ts:1490-1518`). Boundary-split, §7 |
302
| F5 | `/system` (describe the assembled context) | **T (small)** — `coder-session.ts:864-874` |
303
| F6 | `/plugin load <path>` | **T (low)** — `coder-ui.ts:1294-1307` |
304
| F7 | `/goal` | **boundary violation as scoped** — §7 |
305
| F8 | `/reload` | **D (not applicable)** — TS re-execs itself from a source checkout (`coder-ui.ts:1264-1280`). A compiled binary has no equivalent |
306
307
TypeScript defect worth recording while retiring it: `coder-session.ts:968-1019`
308
is about 52 lines of **unreachable duplicate** `/export` and `/help` handlers,
309
guarded out by the live ones at `:910` and `:935`; the dead `/help` carries a
310
stale, shorter text than the live one.
311
312
### G. ACP
313
314
| # | Feature | Disposition |
315
|---|---|---|
316
| G1 | ACP agent discovery, availability checks, and the `acp` tool | **P — coder-lite only.** `acp.rs` 310, `acp_tool.rs` 416, `acp_harness.rs` 60. Nothing to port; this is a coder-lite capability neither other surface has |
317
318
---
319
320
## 4. Disposition counts
321
322
| Disposition | Count |
323
|---|---|
324
| **ported** | **32** |
325
| **dropped** | **10** |
326
| **to-port** | **31** |
327
| **boundary violation as scoped** (not schedulable without a split) | **2** |
328
| **could not determine** | **4** |
329
330
Total feature rows: 79. Dropped items each carry their reason in the matrix:
331
C13, C14, C15, C16 (upstream scope), D18 (text selection wins over mouse), A14
332
and F8 (not applicable to a compiled binary), B4 and §2.4 (no usable source),
333
E17 (out of #117's scope).
334
335
---
336
337
## 5. Which behaviour wins, and what that means for retirement
338
339
#105 said the finished state should be "**one** coder TUI, not two plus a
340
bridge". Three are live. Recording the winner per subsystem so the losers can
341
be retired knowingly rather than left to drift:
342
343
| Subsystem | Winner | Why | Loser's fate |
344
|---|---|---|---|
345
| Composer editing | **coder-lite** (shared composer) | 22 classified chords, persisted history, path and command completion, against a TypeScript composer with no cursor variable | TS composer: retire with `coder-ui.ts` |
346
| Markdown rendering | **coder-lite** (vendored engine) | 24,127 lines, 596 tests, tables/LaTeX/mermaid/CJK against a 490-line hand-written renderer and a 1,120-line per-line lexer | Retire `coder-markdown.ts`; retire `openagents-cli/src/markdown.rs` when `oa coder`'s frame goes |
347
| Hyperlinks | **coder-lite** | Only implementation that emits OSC-8 (`osc8.rs`) | — |
348
| Token accounting display | **coder-lite** | The only one that renders it; TS computes and discards | — |
349
| Diff **content** | **coder-lite** | Syntect-highlighted through the shared engine; `oa coder`'s pane has no highlighting | — |
350
| Diff **navigation** | **`oa coder`** | A real pane with file cycling and side-by-side; coder-lite prints one static block | Port the pane (§6.9), then retire |
351
| PTY / interactive `/run` | **`oa coder`** | Real terminal emulation with 20 tests; coder-lite explicitly does not attempt it | Port (§6.4), then retire |
352
| Status bar composition | **`oa coder`** | Pane-aware hints, segment-dropping under width pressure, an advertised-key test | Port the shape (§6.5); keep coder-lite's amber palette |
353
| Fleet / delegation display | **`coder-ui.ts`** | The only implementation with any; two surfaces, 4 columns, 8 activity renderers | Port (§6.2), then retire |
354
| Paste handling | **`coder-ui.ts`** | The only implementation with bracketed paste, placeholdering, or image drop | Port (§6.3), then retire |
355
| ACP | **coder-lite** | Sole implementation | — |
356
357
Sequence implied: land §6.1 through §6.5 and `coder-ui.ts` has nothing
358
coder-lite lacks except the child view (E2, E3); land §6.9's diff pane and the
359
`oa coder` frame has nothing left either. Retiring a front end is its own issue,
360
not part of #117.
361
362
---
363
364
## 6. The ordered to-port list
365
366
Dependency order, smallest first inside each tier, one landing per group —
367
what #117 asks for. Sizes: **S** under 100 lines, **M** 100 to 400, **L** over
368
400.
369
370
Every entry names the PTY assertion (#116) that proves it. Until #116 lands,
371
none of these may be claimed done (best practice V2).
372
373
### 6.1 — `--offline` on coder-lite · **S** · unblocks #116
374
375
**Files:** `crates/coder-lite/src/main.rs` (parser plus `HELP`),
376
`crates/coder-lite/src/runtime.rs` (select a stand-in reply source),
377
`crates/coder-lite/src/interactive.rs` (skip token validation and the login
378
prompt when offline).
379
380
**Why first:** #116's minimum assertions assume it. Without it the harness
381
needs a provider credential and a network, which makes it not CI-safe, which
382
was the point.
383
384
**Reuse, do not reinvent:** `cli.rs:814-832` already carries the stand-in
385
answer text and its rationale. The offline reply source is runtime, so it
386
belongs in `openagents-cli` and coder-lite selects it — see §7.3.
387
388
**PTY assertion:** `coder-lite --offline` starts, renders a composer, accepts a
389
typed prompt, submits it, and the transcript shows the stand-in reply — all
390
with no `OPENAGENTS_API_KEY` in the environment and no listener on the API
391
origin.
392
393
### 6.2 — Live fleet rows during `/delegate` fan-out · **M** · highest product value
394
395
**Files:** `crates/coder-lite/src/runtime.rs` (new `Control` variants),
396
`crates/coder-lite/src/tui.rs` (a fleet block),
397
`crates/coder-lite/src/interactive.rs` (`apply` arms), and one call site in
398
`crates/openagents-cli/src/delegate.rs:1604`.
399
400
**Current behaviour:** `delegate x3 <prompt>` renders one tool header and a
401
5-line box that stays empty until every child finishes, then fills with a wall
402
of text of which the last 5 lines are visible. `Control` (`runtime.rs:43-70`)
403
has ten variants and none carries child state. `ui.agents` (`tui.rs:177`) is
404
assigned once and read never.
405
406
**Why it is cheap:** the machinery already exists and is already consumed.
407
`ChildEvent` (`delegate.rs:83-97`) carries `Started { id, lane, workspace, pid }`,
408
`Output { id, text }`, `Activity { id, text }`, and `Finished`.
409
`DelegationSupervisor::dispatch_streaming(prompt, events: mpsc::UnboundedSender<ChildEvent>)`
410
exists at `delegate.rs:387`, and `oa delegate` already renders from it. The only
411
reason coder-lite sees nothing is that `fanout_for_tool` (`delegate.rs:1578`)
412
calls the blocking `dispatch()` at `:1604` and returns one string at the end.
413
414
**Boundary:** the `openagents-cli` edit plumbs an existing event channel
415
outward; it does not move logic inward. The supervisor, the lanes, the child
416
options, and the refusals stay where they are. Give `fanout_for_tool` an
417
optional `ChildEvent` sink so the existing blocking signature keeps working for
418
callers that do not want rows.
419
420
**Shape to copy** (`coder-fleet.ts:210-233`): 4 columns — a tree branch, a
421
status mark with 5 states (pending, running, completed, failed, stopped), a
422
`description` padded to `min(28, max(12, floor(room * 0.35)))`, and a tail of
423
`taskActivity` plus `taskCounters`. Cap the block at 8 rows (`FLEET_ROWS_MAX`)
424
with a `+N more` line, and 3 activity preview lines per child (`PREVIEW_ROWS`).
425
Skip the 34-column sidebar for now — it is a second layout mode, and the inline
426
block is the load-bearing half.
427
428
**PTY assertion:** with a fixture fan-out of 3 children, the frame shows 3
429
fleet rows within one draw of the first `Started`; a row's mark changes from
430
running to completed when its child finishes; and the block never exceeds 8
431
rows for a fan-out of 12.
432
433
### 6.3 — Bracketed paste, paste placeholders, image paste · **M**
434
435
**Files:** `crates/coder-lite/src/interactive.rs` (enable the mode, read
436
`Event::Paste`), `crates/coder-lite/src/tui.rs` (render the placeholder token),
437
plus a new paste-token module.
438
439
**Current behaviour:** grep confirms `EnableBracketedPaste`, `Event::Paste`,
440
and `2004` appear **nowhere** in `crates/`. `interactive.rs:166` is
441
`let Event::Key(key) = event::read()? else { continue; }`, so a paste event
442
would be dropped even if the mode were on. Today a multi-line paste arrives as
443
a stream of individual key events and the first embedded newline **submits the
444
prompt mid-paste**.
445
446
**Port from** `coder-paste.ts`: `PASTE_TEXT_THRESHOLD = 800` characters,
447
`PASTE_MAX_LINES = 1`, the `[Pasted text #1]` / `[Pasted text #1 +10 lines]`
448
placeholder, expansion at submit, and whole-token backspace. From
449
`coder-image.ts`: dropped-path detection for `png|jpe?g|gif|webp`, quote
450
stripping, backslash unescaping, `[Image #N]`.
451
452
**Boundary:** the placeholder token, its map, and its rendering are
453
presentation and belong here. Turning an image path into a content part (MIME
454
sniffing, base64, the multimodal message shape) is runtime and belongs in
455
`openagents-cli` — see §7.2.
456
457
**PTY assertion:** writing a bracketed-paste sequence carrying two lines leaves
458
the composer holding two lines and **does not submit**; a 900-character paste
459
renders as a `[Pasted text #1 +N lines]` token; one backspace removes the whole
460
token.
461
462
### 6.4 — `/run` under a pseudoterminal · **L**
463
464
**Files:** a new pane in `crates/coder-lite/src/tui.rs`, pane dispatch in
465
`crates/coder-lite/src/interactive.rs`, `commands.rs` `/run` rewired.
466
467
**Current behaviour:** `commands.rs:211-300` spawns `/bin/sh -c` with piped
468
stdio into the 5-line box, and the doc comment states the trade honestly ("Not
469
a pseudoterminal: there is no pane to attach to and nothing takes keys while it
470
runs"). The cost is the usual one: `git` drops its colour, `top` and `vim`
471
refuse to draw, and anything that asks the kernel for its width gets nothing.
472
473
**Reuse:** `openagents-cli/src/pty.rs` is 753 lines and complete —
474
`PtySession` over `portable-pty`, `PtyScreen` over `vt100::Parser`, `resize`
475
raising a real `SIGWINCH`, `DETACH = Ctrl+]`, and a 19-arm `encode_key`. It is
476
already a library module of the crate coder-lite depends on, so this is pane
477
plumbing, not a port of the emulator.
478
479
**Do this after 6.2**, because both introduce a second thing competing for the
480
transcript's rows and the fleet block is the cheaper one to get the layout
481
right on.
482
483
**PTY assertion:** `/run stty size` in an 80x24 harness prints the pane's inner
484
dimensions, not the host's; `/run cat` echoes typed characters; Ctrl+] kills the
485
child and returns keys to the composer; resizing the harness mid-run re-reports
486
the new size to a `trap 'stty size' WINCH` script.
487
488
### 6.5 — The status bar: model, lane, repo, branch, reasoning, state, elapsed, hints · **M**
489
490
**Files:** `crates/coder-lite/src/tui.rs` (`render` status region),
491
`crates/coder-lite/src/interactive.rs` (stop dropping the facts it already
492
has).
493
494
**Current behaviour:** one right-aligned row of
495
`{p} prompt + {c} completion = {t} tokens` (`tui.rs:342-352`). Five facts the
496
frame **already holds** are rendered nowhere: `model` (E5), `repo` and `branch`
497
(E7), `reasoning` (E8, read by nothing at all), and `agents` (E18). The lane is
498
printed once at startup and never again.
499
500
**Shape to copy:** `oa coder` `tui.rs:648-725` — separated segments in priority
501
order, whole segments dropped from the end when the row narrows, then hints
502
dropped from the end. Keep coder-lite's amber palette; take the composition,
503
not the colours (#105 pins the palette as the product).
504
505
Land B3 with it: a test that presses every key `commands.rs:KEYS` advertises
506
and asserts something changed, matching `oa coder`'s
507
`every_key_the_status_bar_names_does_something`.
508
509
**PTY assertion:** after one offline turn the status row contains the model
510
name, the lane label, and the branch; narrowing the harness to 40 columns drops
511
whole segments and never renders a partial one; every row is exactly the window
512
width.
513
514
### 6.6 — Tool-call collapse/expand, status marks, and the 5-row padding fix · **M**
515
516
**Files:** `crates/coder-lite/src/tui.rs` (`render_entry`, `Role::Tool` arm),
517
`crates/coder-lite/src/interactive.rs` (a toggle key).
518
519
Three defects in one arm (`tui.rs:437-467`):
520
521
1. `for i in 0..5` emits exactly five rows whatever the output length, so a
522
   one-line result costs four blank transcript rows (D10).
523
2. There is no way to see more than the last five lines of any tool result
524
   (D8). TS keys this on Ctrl+O against the newest call (`coder-ui.ts:1371`).
525
3. A running call looks identical to a finished one; only failure is marked,
526
   and only as the text `— failed` (D9). TS uses three distinct marks.
527
528
**PTY assertion:** a tool result of one line renders one output row, not five;
529
Ctrl+O on the newest tool call grows the box and a second press shrinks it; a
530
call that is still running shows a different mark from one that has returned.
531
532
### 6.7 — Honour `NO_COLOR` · **S**
533
534
**Files:** `crates/coder-lite/src/markdown/theme.rs` (`amberize`),
535
`crates/coder-lite/src/tui.rs` (chrome styles).
536
537
The detection is already ported and already correct (`markdown/colors.rs:53-95`,
538
`NO_COLOR` checked first at `:56`) and then discarded: `transcript.rs:277` runs
539
`amberize`, which sets every span to `Rgb(255,176,0)` on `Rgb(8,6,0)`
540
(`theme.rs:112-123`), and `tui.rs` hardcodes the same pair for all chrome.
541
`NO_COLOR=1` currently changes nothing.
542
543
Gate `amberize` and the chrome styles on the detected level: at
544
`ColorLevel::None` emit `Color::Reset` and keep the modifiers, which is exactly
545
what `oa coder` does over the finished buffer (`tui.rs:41-50` `drain_color`).
546
547
**PTY assertion:** with `NO_COLOR=1` in the child's environment, the captured
548
frame contains no SGR colour parameters and still contains bold and dim
549
modifiers.
550
551
### 6.8 — Interrupt a running turn · **S**, possibly **M** (see §8.3)
552
553
Today every exit key leaves the session (`interactive.rs:177-179`, checked
554
before the composer sees the key), so the only way to stop a turn is to end it.
555
TS gives Esc and Ctrl+C a first meaning of "interrupt", falling through to exit
556
when there is nothing to interrupt (`coder-ui.ts:1347-1369`).
557
558
Whether cancellation is even available is an open question — see §8.3.
559
560
**PTY assertion:** Esc during a streaming offline turn stops the stream, posts
561
a notice, and leaves the session up; a second Esc exits.
562
563
### 6.9 — Second tier (schedule after the first)
564
565
| Item | Size | Note |
566
|---|---|---|
567
| Diff inspector pane (D11) | **L** | Port `oa coder`'s pane; add the syntect highlighting it lacks and coder-lite already has |
568
| `/delegate` as a typed command (F3) | **S** | Pairs with 6.2 |
569
| Child full-screen transcript view plus Ctrl+X stop (E2, E3) | **M** | Depends on 6.2 |
570
| Reasoning summaries in the transcript (E10) | **M** | `Role::Reasoning` is already styled and constructed only by a test |
571
| Scroll indicator, Home/End (E12, E13) | **S** | One landing |
572
| `/system` (F5) | **S** | — |
573
| `/skills` screen (F4) | **M** | Boundary-split, §7.4 |
574
| Splash / empty state (D16) | **S** | Cosmetic |
575
| Kill-ring/yank, undo/redo (A12, A13) | **M** | Harvest from upstream `textarea.rs` |
576
| Transcript search (D15) | **M** | Nothing here has it |
577
| Word-level diff (D12) | **M** | Nothing here has it |
578
| `/plugin load` (F6) | **S** | — |
579
| Box frame with badge title (D17) | **S** | **Owner-gated** — #105 pins the frame as the product |
580
581
---
582
583
## 7. Boundary violations flagged rather than scheduled
584
585
Per #117's guardrail. After release 0.0.2 this is a module seam inside one
586
binary rather than a seam between two, which makes it easier to cross by
587
accident and no less real.
588
589
**7.1 — `/goal` (F7), as scoped, is a violation.** TS's `/goal` has five
590
sub-actions and a token budget backed by a `GoalStore` (`coder-goals.ts`). A
591
goal store with budgets is session state and spending policy — runtime. If this
592
is wanted, the store lands in `openagents-cli` and coder-lite renders the goal
593
in the status bar and dispatches the sub-commands. Porting `coder-goals.ts` into
594
`crates/coder-lite/` would put budget policy in the presentation crate.
595
596
**7.2 — Image paste (A11) must be split.** The placeholder token, its map, and
597
its rendering are presentation. MIME sniffing, base64 encoding, and assembling a
598
multimodal content part are runtime and belong beside the other message
599
construction in `openagents-cli`. Do not let `crates/coder-lite/` learn the
600
provider's content-part shape.
601
602
**7.3 — `--offline` (6.1) is a near miss.** The flag and the "am I offline"
603
branch are the session loop's, so they are coder-lite's. The stand-in reply
604
source is a reply source — the same category as the real one — and belongs in
605
`openagents-cli` next to `run_offline_coder` (`cli.rs:1411`) and the answer text
606
already written at `cli.rs:814-832`. Writing a second stand-in inside coder-lite
607
would be two implementations of one thing, which is precisely what
608
`Cargo.toml`'s dependency comment exists to prevent.
609
610
**7.4 — `/skills` (F4) must be split.** The screen is presentation. The skill
611
catalog, the enable/disable decision, and its effect on the assembled prompt are
612
runtime; coder-lite reads a list and reports a toggle.
613
614
**7.5 — Fleet rows (6.2) are *not* a violation, and the reasoning is worth
615
keeping.** The change touches `openagents-cli/src/delegate.rs`, but it plumbs an
616
existing event channel outward to a renderer. No supervision, lane selection,
617
child-option resolution, or refusal moves. The test of the rule is direction:
618
logic moving *into* coder-lite is the violation; a fact moving *out* to be drawn
619
is the boundary working.
620
621
---
622
623
## 8. What I could not determine
624
625
Four things. Recorded rather than guessed, per the honesty contract.
626
627
**8.1 — Whether coder-lite orphans child processes on exit.** TS explicitly
628
kills the fleet in teardown (`session.stopTasks()`, `coder-ui.ts:568-571`, "the
629
fleet dies with the console"). coder-lite's exit path awaits `session.finish()`
630
under a 10-second `REVOCATION_GRACE` (`interactive.rs:247-262`), which ends the
631
*thread*. Children are spawned inside the tool future by the supervisor;
632
whether they are killed, detached, or left running when the TUI exits is not
633
visible from the frame code, and I did not trace `DelegationSupervisor`'s drop
634
behaviour. Establish this before 6.2 ships — a fleet display that shows children
635
a reader cannot stop is worse than none.
636
637
**8.2 — Whether the thread id (E16) is available to the frame at all.**
638
`Control` has no `Thread` variant and no implementation renders one. Whether
639
`Session` holds an id it could send was not traced; if it does not, E16 needs a
640
runtime change and is a larger item than its status-bar row suggests.
641
642
**8.3 — Whether a turn can be cancelled (6.8).** `submit` spawns
643
`session.execute_turn(&text, tx)` on a detached tokio task
644
(`interactive.rs:517-519`) and keeps no handle. Interruption may require a
645
cancellation token in the runtime, which would move 6.8 from **S** to **M** and
646
put part of it on the other side of the seam.
647
648
**8.4 — Whether `oa coder`'s frame has any user after release 0.0.2.** The
649
release makes `coder-lite` the shipped artifact answering both fronts. Whether
650
`oa coder`'s TUI (`openagents-cli/src/{tui,interactive,diff,pty}.rs`, 4,056
651
lines, 189 tests) is still reachable from a shipped binary after that, or becomes
652
a library of parts that §6.4 and §6.9 harvest from, is an owner question. It
653
changes whether "retire it" in §5 means deleting a front end or absorbing one.
654
655
---
656
657
## 9. Landing rule for the porting half
658
659
Restating #117's acceptance so a later reader does not have to reconstruct it:
660
661
- Each `to-port` entry lands with the PTY assertion named beside it in §6, or is
662
  re-dispositioned here with a reason.
663
- `pnpm run check` green per landing (`fmt:check`, `lint`, `check:fast`,
664
  `typecheck`, `test`); the components alone are not the gate.
665
- No entry closes on headless evidence — best practice V2. The 15 `TestBackend`
666
  frame tests coder-lite has today are not that evidence, which is the whole
667
  reason #116 exists.

This page updates live while a promote is in flight · changelog