From b3be4f33fe710f7a3b2aeac489f6d267bcf50337 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 16 Oct 2026 15:16:15 -0700 Subject: [PATCH 1/4] refactor(tui): rename mark and ramp glyph modules The glyph modules keep their names aligned with what they draw. Call sites follow the paths. Behavior is unchanged. Refs CL-10640 --- src/tui/chrome-state.ts | 2 +- src/tui/landing.test.ts | 2 +- src/tui/landing.ts | 2 +- src/tui/lockup.test.ts | 2 +- src/tui/lockup.ts | 13 +++++++++---- src/tui/mark-anim.test.ts | 2 +- src/tui/{mark-anim.ts => mark-raster.ts} | 0 src/tui/provider/surface.ts | 2 +- src/tui/{ramp.ts => ramp-glyphs.ts} | 0 src/tui/ramp-paint.test.ts | 2 +- src/tui/ramp.test.ts | 2 +- src/tui/render-loop.test.ts | 2 +- src/tui/runtime-bridge.ts | 2 +- src/tui/shell/chrome.ts | 2 +- src/tui/shell/internals.ts | 2 +- src/tui/welcome.ts | 2 +- src/tui/width-columns.test.ts | 2 +- 17 files changed, 23 insertions(+), 18 deletions(-) rename src/tui/{mark-anim.ts => mark-raster.ts} (100%) rename src/tui/{ramp.ts => ramp-glyphs.ts} (100%) diff --git a/src/tui/chrome-state.ts b/src/tui/chrome-state.ts index 6988f19d3..df687e301 100644 --- a/src/tui/chrome-state.ts +++ b/src/tui/chrome-state.ts @@ -20,7 +20,7 @@ import { } from "./geometry/zones.js"; import { CREDENTIAL_FAILURE_USER_MESSAGE } from "../inference-error-message.js"; import type { Telemetry } from "../telemetry/index.js"; -import type { RampPhase } from "./ramp.js"; +import type { RampPhase } from "./ramp-glyphs.js"; /** * Linger window for a finished agent row after `finishedAt`; mid of the diff --git a/src/tui/landing.test.ts b/src/tui/landing.test.ts index ded7ae548..a0d5997c5 100644 --- a/src/tui/landing.test.ts +++ b/src/tui/landing.test.ts @@ -41,7 +41,7 @@ import { } from "./landing"; import { LOCKUP_WORDMARK } from "./lockup"; import { MARK_LARGE, MARK_MID, MARK_SMALL } from "./mark-shape"; -import { SNOW_CHAR } from "./mark-anim"; +import { SNOW_CHAR } from "./mark-raster"; const SIZE = { width: 80, height: 24 } as const; const NOTICE = "Anonymous usage telemetry is enabled. Disable in /settings."; diff --git a/src/tui/landing.ts b/src/tui/landing.ts index 6b8f10a8a..5097599b3 100644 --- a/src/tui/landing.ts +++ b/src/tui/landing.ts @@ -19,7 +19,7 @@ import { MARK_SMALL, type MarkGrid, } from "./mark-shape.js"; -import { renderMark } from "./mark-anim.js"; +import { renderMark } from "./mark-raster.js"; import { UI } from "./theme.js"; import { stringWidth } from "./view/height.js"; diff --git a/src/tui/lockup.test.ts b/src/tui/lockup.test.ts index bf7264391..f8f6ed42a 100644 --- a/src/tui/lockup.test.ts +++ b/src/tui/lockup.test.ts @@ -11,7 +11,7 @@ import { STALL_BLINK_BURST_MS, STALL_BLINK_CYCLE_MS, type RampPhase, -} from "./ramp"; +} from "./ramp-glyphs"; import { UI } from "./theme"; const idle = (nowMs: number): LockupInput => ({ diff --git a/src/tui/lockup.ts b/src/tui/lockup.ts index 0cbe32882..7707aa8d1 100644 --- a/src/tui/lockup.ts +++ b/src/tui/lockup.ts @@ -4,17 +4,22 @@ * It rides the prompt box's bottom border. Idle it reads `corbits code`; * while a turn runs it reads the live phase — `thinking`, `responding`, * the running tool's name — led by a single density cell (`rampPulse` in - * `ramp.ts`): the word alone printed the same static `working` for a + * `ramp-glyphs.ts`): the word alone printed the same static `working` for a * live run and a hung one. The cell cycles while the turn moves, holds a * static half block on an operator gate, and blinks a bang when stalled - * (glyph and motion carry the states — see ramp.ts). + * (glyph and motion carry the states — see ramp-glyphs.ts). * * Cell and word share the phase's color, so the slot never disagrees * with it. Pure and clock-injected: `nowMs` in, cells out. */ -import { type MarkCell } from "./mark-anim.js"; -import { rampFg, rampPulse, type RampPhase, type StallAge } from "./ramp.js"; +import { type MarkCell } from "./mark-raster.js"; +import { + rampFg, + rampPulse, + type RampPhase, + type StallAge, +} from "./ramp-glyphs.js"; import { UI } from "./theme.js"; import { stringWidth } from "./view/height.js"; diff --git a/src/tui/mark-anim.test.ts b/src/tui/mark-anim.test.ts index 0ef9deaf9..7a5582293 100644 --- a/src/tui/mark-anim.test.ts +++ b/src/tui/mark-anim.test.ts @@ -7,7 +7,7 @@ import { markText, renderMark, smooth, -} from "./mark-anim"; +} from "./mark-raster"; import { MARK_COLS, MARK_LARGE, MARK_ROWS } from "./mark-shape"; import { UI } from "./theme"; diff --git a/src/tui/mark-anim.ts b/src/tui/mark-raster.ts similarity index 100% rename from src/tui/mark-anim.ts rename to src/tui/mark-raster.ts diff --git a/src/tui/provider/surface.ts b/src/tui/provider/surface.ts index 1d4e4b2d1..24d1856af 100644 --- a/src/tui/provider/surface.ts +++ b/src/tui/provider/surface.ts @@ -16,7 +16,7 @@ import { PRODUCT_NAME } from "../../branding.js"; import { ollamaDiscoveryFailureLine } from "../../provider/ollama.js"; import { TELEMETRY_NOTICE } from "../../telemetry/index.js"; import { wrapLines } from "../view/height.js"; -import { rampFor, rampLine } from "../ramp.js"; +import { rampFor, rampLine } from "../ramp-glyphs.js"; import { destroySubtree } from "../teardown.js"; import { UI } from "../theme.js"; import { diff --git a/src/tui/ramp.ts b/src/tui/ramp-glyphs.ts similarity index 100% rename from src/tui/ramp.ts rename to src/tui/ramp-glyphs.ts diff --git a/src/tui/ramp-paint.test.ts b/src/tui/ramp-paint.test.ts index 7f8be6676..de437ac98 100644 --- a/src/tui/ramp-paint.test.ts +++ b/src/tui/ramp-paint.test.ts @@ -8,7 +8,7 @@ import { describe, expect, test } from "bun:test"; import { withTestRenderer } from "./harness"; -import { RAMP_CYCLE_MS } from "./ramp"; +import { RAMP_CYCLE_MS } from "./ramp-glyphs"; import { attachSessionBridge, createRecordingPort } from "./runtime-bridge"; import { createAppShell } from "./shell/index"; diff --git a/src/tui/ramp.test.ts b/src/tui/ramp.test.ts index d3c055aa8..04168c96f 100644 --- a/src/tui/ramp.test.ts +++ b/src/tui/ramp.test.ts @@ -11,7 +11,7 @@ import { renderRamp, STALL_BLINK_BURST_MS, STALL_BLINK_CYCLE_MS, -} from "./ramp"; +} from "./ramp-glyphs"; import { UI } from "./theme"; const BRAILLE = /[⠀-⣿]/; diff --git a/src/tui/render-loop.test.ts b/src/tui/render-loop.test.ts index 9db380696..cd19c36e4 100644 --- a/src/tui/render-loop.test.ts +++ b/src/tui/render-loop.test.ts @@ -9,7 +9,7 @@ import { describe, expect, test } from "bun:test"; import { withTestRenderer } from "./harness"; -import { RAMP_CYCLE_MS, rampPulse } from "./ramp"; +import { RAMP_CYCLE_MS, rampPulse } from "./ramp-glyphs"; import { attachSessionBridge, createRecordingPort } from "./runtime-bridge"; import { appendStreamRow } from "./shell/chrome"; import { createAppShell } from "./shell/index"; diff --git a/src/tui/runtime-bridge.ts b/src/tui/runtime-bridge.ts index 0e62f1075..8aae8a066 100644 --- a/src/tui/runtime-bridge.ts +++ b/src/tui/runtime-bridge.ts @@ -38,7 +38,7 @@ import { } from "./shell/internals.js"; import { applyShellInterrupt, surfaceSystemNotice } from "./shell/prompt.js"; import { streamRowAt, streamRowCount } from "./shell/transcript.js"; -import { rampAnimating } from "./ramp.js"; +import { rampAnimating } from "./ramp-glyphs.js"; import { onTurnBoundary } from "../agent/reactor-events.js"; import { resolveRampPhase, diff --git a/src/tui/shell/chrome.ts b/src/tui/shell/chrome.ts index ebba66383..7625f81bc 100644 --- a/src/tui/shell/chrome.ts +++ b/src/tui/shell/chrome.ts @@ -34,7 +34,7 @@ import { lockupWidth, type LockupInput, } from "../lockup.js"; -import type { RampPhase, StallAge } from "../ramp.js"; +import type { RampPhase, StallAge } from "../ramp-glyphs.js"; import { BORDER, composeAttentionLabel, diff --git a/src/tui/shell/internals.ts b/src/tui/shell/internals.ts index e673e3b75..53f1c644c 100644 --- a/src/tui/shell/internals.ts +++ b/src/tui/shell/internals.ts @@ -17,7 +17,7 @@ import { import { type SentHistoryBrowse } from "../sent-message-history.js"; import { type PromptRecognitionSource } from "../prompt-recognition.js"; import { type PromptInput } from "../prompt-input.js"; -import type { RampPhase, StallAge } from "../ramp.js"; +import type { RampPhase, StallAge } from "../ramp-glyphs.js"; import type { ActivityState } from "../chrome-state.js"; import { type CostContextMeter } from "../prompt-border.js"; import { type FocusState } from "../focus/index.js"; diff --git a/src/tui/welcome.ts b/src/tui/welcome.ts index d6f944062..d28746d75 100644 --- a/src/tui/welcome.ts +++ b/src/tui/welcome.ts @@ -22,7 +22,7 @@ import { } from "@opentui/core"; import { PRODUCT_NAME } from "../branding.js"; -import { MARK_PERIOD_SECONDS, renderMark } from "./mark-anim.js"; +import { MARK_PERIOD_SECONDS, renderMark } from "./mark-raster.js"; import { MARK_LARGE, MARK_MID, diff --git a/src/tui/width-columns.test.ts b/src/tui/width-columns.test.ts index 07775d524..b35c09a8c 100644 --- a/src/tui/width-columns.test.ts +++ b/src/tui/width-columns.test.ts @@ -18,7 +18,7 @@ import { middleEllipsis } from "./command-display.js"; import { renderDiff } from "./diff.js"; import { wrapLanding } from "./landing.js"; import { lockupWidth } from "./lockup.js"; -import type { RampPhase } from "./ramp.js"; +import type { RampPhase } from "./ramp-glyphs.js"; import { formatPaletteRows } from "./command-catalog.js"; import { composeDecisionBody, wrapWords } from "./overlay-body.js"; import { thinkingLivePreviewLines, thinkingSettledLine } from "./thinking.js"; From 1ebd50bf76dda39b48c70f348719744c5396fbc8 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 16 Oct 2026 15:19:54 -0700 Subject: [PATCH 2/4] docs(tui): describe motion without the sticky poll Wording only, so the behavior diff can stay within the file cap. The modules still poll until that change lands. Refs CL-10640 --- docs/TUI-inline-eval.md | 4 ++-- docs/TUI.md | 8 ++++---- src/tui/chrome-repaint.test.ts | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/TUI-inline-eval.md b/docs/TUI-inline-eval.md index 59a30d764..873162025 100644 --- a/docs/TUI-inline-eval.md +++ b/docs/TUI-inline-eval.md @@ -36,14 +36,14 @@ A raw scrollback log also has no place for the shell-output preview the host pai - `src/tui/shell/layout.ts` applies terminal geometry: floors, prompt-row sync, landing split, relayout inputs. - `src/tui/geometry/zones.ts` defines the floors and the collapse order the layout refuses to violate. - `src/tui/shell/chrome.ts` `relayout` is the live entry that re-resolves the tree when visibility, prompt rows, or overlay mode change. -- The `stickyPoll` interval in `src/tui/product-host.ts` keeps the shell-output preview and sticky chrome clocks painted. It is not scrollback. +- A Timeline in `src/tui/product-host.ts` keeps the shell-output preview's sibling chrome clocks painted while a lane is live. It is not scrollback. ## Deleted if inline mode were adopted It is not adopted. If it were, the deletion this record refuses is: - `shell/layout.ts`, the `geometry/zones.ts` floors, and `chrome.ts` relayout would be deleted or bypassed for a scrollback-only writer. -- `stickyPoll` would go, because a raw scrollback commit has no sticky zone to repaint. +- The live chrome Timeline would go, because a raw scrollback commit has no sticky zone to repaint. Those files stay. Bypassing them for `writeToScrollback` would drop the floors, the single scroll lease, overlay selectors, and the prompt kill ring. None of that is implied by a scrollback snapshot. diff --git a/docs/TUI.md b/docs/TUI.md index 05abe1b2c..a4ebcff2e 100644 --- a/docs/TUI.md +++ b/docs/TUI.md @@ -116,11 +116,11 @@ plus up to three dim tail lines of the command's live output and — when the feed window holds more than three lines — the same dim `⋯ +N lines` elision marker as the settle preview, `N` counting lines within the live window (the feed keeps only the most recent 8 KiB). The output -travels through a polled bounded feed (`src/session/shell-output-feed.ts`), +travels through a bounded feed (`src/session/shell-output-feed.ts`), not a reactor event: the plugin appends chunks (capped at an 8 KiB tail per call, emitting at most once per 100 ms plus a final flush at settle), and -the product host's sticky poll reads the feed snapshot every 200 ms and -repaints the pending row frame-coalesced. Nothing from the feed is +the product host repaints the pending row on the next frame when the feed +appends. Nothing from the feed is persisted. When the feed is not wired (tests, the demo shell), the row renders exactly as before — silent degradation. @@ -160,7 +160,7 @@ on `LIVE_WORD_MS`; gated turns still read `waiting` or `stopping`. `ACTIVITY_STATES` exported from the session chrome module is the source of truth for what the slot can say, not this list. It is led by a single density cell -(`rampPulse`, `src/tui/ramp.ts`). The cell, not the word, +(`rampPulse`, `src/tui/ramp-glyphs.ts`). The cell, not the word, is what says whether the session is healthy, and it carries four states: | State | Cell | Reads as | diff --git a/src/tui/chrome-repaint.test.ts b/src/tui/chrome-repaint.test.ts index 81b5e9176..295ed62c1 100644 --- a/src/tui/chrome-repaint.test.ts +++ b/src/tui/chrome-repaint.test.ts @@ -42,7 +42,8 @@ describe("chrome repaint gate", () => { await withShell((shell) => { paintChrome(shell); const baseline = chromeComposeCount(shell); - // What stickyPoll does every 200ms while fully idle. + // Idle hosts used to call paintChrome on a 200ms poll. Repeating it + // must still recompose nothing. for (let tick = 0; tick < 10; tick++) paintChrome(shell); expect(chromeComposeCount(shell)).toBe(baseline); }); From 9b54513e827ed29644ed3786070cfe13ded6c8ee Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 16 Oct 2026 15:20:33 -0700 Subject: [PATCH 3/4] docs(tui): align the sticky chrome comment with the clock Wording only. The host still polls until the behavior change lands. Refs CL-10640 --- src/tui/chrome-state.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tui/chrome-state.ts b/src/tui/chrome-state.ts index df687e301..6c63085d3 100644 --- a/src/tui/chrome-state.ts +++ b/src/tui/chrome-state.ts @@ -152,8 +152,8 @@ export function formatChromeZones( } /** True while the strip needs wall-clock ticks: any live worker or a - * finished row inside the linger window. Product host polls on it to keep - * clocks fresh and freeze `syncAgentProgress` rewrites. */ + * finished row inside the linger window. The product host runs a Timeline + * while this is true and skips `syncAgentProgress` rewrites for that span. */ export function agentsChromeNeedsSticky( agents: readonly ChromeAgentSession[] | null | undefined, nowMs: number, From 9f2af8991006d81ead3b2de881e5f058071b1a74 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 16 Oct 2026 15:35:27 -0700 Subject: [PATCH 4/4] docs(tui): restore sticky poll wording on the rename The host still polls every 200ms. Timeline and feed-append wording does not belong on a rename that leaves that poll in place. Refs CL-10640 --- docs/TUI-inline-eval.md | 4 ++-- docs/TUI.md | 6 +++--- src/tui/chrome-repaint.test.ts | 3 +-- src/tui/chrome-state.ts | 4 ++-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/TUI-inline-eval.md b/docs/TUI-inline-eval.md index 873162025..59a30d764 100644 --- a/docs/TUI-inline-eval.md +++ b/docs/TUI-inline-eval.md @@ -36,14 +36,14 @@ A raw scrollback log also has no place for the shell-output preview the host pai - `src/tui/shell/layout.ts` applies terminal geometry: floors, prompt-row sync, landing split, relayout inputs. - `src/tui/geometry/zones.ts` defines the floors and the collapse order the layout refuses to violate. - `src/tui/shell/chrome.ts` `relayout` is the live entry that re-resolves the tree when visibility, prompt rows, or overlay mode change. -- A Timeline in `src/tui/product-host.ts` keeps the shell-output preview's sibling chrome clocks painted while a lane is live. It is not scrollback. +- The `stickyPoll` interval in `src/tui/product-host.ts` keeps the shell-output preview and sticky chrome clocks painted. It is not scrollback. ## Deleted if inline mode were adopted It is not adopted. If it were, the deletion this record refuses is: - `shell/layout.ts`, the `geometry/zones.ts` floors, and `chrome.ts` relayout would be deleted or bypassed for a scrollback-only writer. -- The live chrome Timeline would go, because a raw scrollback commit has no sticky zone to repaint. +- `stickyPoll` would go, because a raw scrollback commit has no sticky zone to repaint. Those files stay. Bypassing them for `writeToScrollback` would drop the floors, the single scroll lease, overlay selectors, and the prompt kill ring. None of that is implied by a scrollback snapshot. diff --git a/docs/TUI.md b/docs/TUI.md index a4ebcff2e..15f269048 100644 --- a/docs/TUI.md +++ b/docs/TUI.md @@ -116,11 +116,11 @@ plus up to three dim tail lines of the command's live output and — when the feed window holds more than three lines — the same dim `⋯ +N lines` elision marker as the settle preview, `N` counting lines within the live window (the feed keeps only the most recent 8 KiB). The output -travels through a bounded feed (`src/session/shell-output-feed.ts`), +travels through a polled bounded feed (`src/session/shell-output-feed.ts`), not a reactor event: the plugin appends chunks (capped at an 8 KiB tail per call, emitting at most once per 100 ms plus a final flush at settle), and -the product host repaints the pending row on the next frame when the feed -appends. Nothing from the feed is +the product host's sticky poll reads the feed snapshot every 200 ms and +repaints the pending row frame-coalesced. Nothing from the feed is persisted. When the feed is not wired (tests, the demo shell), the row renders exactly as before — silent degradation. diff --git a/src/tui/chrome-repaint.test.ts b/src/tui/chrome-repaint.test.ts index 295ed62c1..81b5e9176 100644 --- a/src/tui/chrome-repaint.test.ts +++ b/src/tui/chrome-repaint.test.ts @@ -42,8 +42,7 @@ describe("chrome repaint gate", () => { await withShell((shell) => { paintChrome(shell); const baseline = chromeComposeCount(shell); - // Idle hosts used to call paintChrome on a 200ms poll. Repeating it - // must still recompose nothing. + // What stickyPoll does every 200ms while fully idle. for (let tick = 0; tick < 10; tick++) paintChrome(shell); expect(chromeComposeCount(shell)).toBe(baseline); }); diff --git a/src/tui/chrome-state.ts b/src/tui/chrome-state.ts index 6c63085d3..df687e301 100644 --- a/src/tui/chrome-state.ts +++ b/src/tui/chrome-state.ts @@ -152,8 +152,8 @@ export function formatChromeZones( } /** True while the strip needs wall-clock ticks: any live worker or a - * finished row inside the linger window. The product host runs a Timeline - * while this is true and skips `syncAgentProgress` rewrites for that span. */ + * finished row inside the linger window. Product host polls on it to keep + * clocks fresh and freeze `syncAgentProgress` rewrites. */ export function agentsChromeNeedsSticky( agents: readonly ChromeAgentSession[] | null | undefined, nowMs: number,