From b340b19d01c16914e9afadd3f89e8a36ea87d665 Mon Sep 17 00:00:00 2001 From: minhn4 Date: Sun, 2 Aug 2026 19:00:51 +0700 Subject: [PATCH 1/2] feat(tui): keep the footer key hints visible after the first response The prompt footer's right slot swapped the shift+tab modes / tab agents hints for the context/cost readout as soon as the session had any assistant output tokens, and they never came back for the rest of the session. The slot now renders the two independently instead of either/or: from 120 columns up it shows the key hints and the usage readout side by side; on narrower terminals it shows the hints while the session is idle and the usage readout while it is busy, with the hints as the fallback before any usage exists. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 9 +++ DIVERGENCES.md | 3 +- packages/tui/src/component/prompt/index.tsx | 45 +++++++++----- packages/tui/test/footer-width.view.test.tsx | 63 +++++++++++++++----- 4 files changed, 87 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a621432b7c..c4bbe351ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ file is the human-readable summary of what changed and why. ## Unreleased +### TUI + +- **The `shift+tab modes` / `tab agents` hints survive the first response** — + previously the prompt footer's right slot swapped them for the context/cost + readout as soon as the session had any assistant output, and they never came + back. From 120 columns up the slot now shows the key hints and the usage + readout side by side; on narrower terminals it shows the hints while the + session is idle and the usage readout while it is busy. + ## 1.18.4-5 ### TUI diff --git a/DIVERGENCES.md b/DIVERGENCES.md index a2325f9f49..1a7caed18e 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -124,6 +124,7 @@ from the squash message first, or use a plain merge. | `packages/tui/src/component/use-connected.tsx`, `packages/tui/src/context/local.tsx` | use-connected.tsx: the provider predicate is extracted into an exported `isConnectedProvider`; `useConnected` now delegates to it (same logic). local.tsx: `fallbackModel`'s last resort is `sync.data.provider.find(isConnectedProvider) ?? sync.data.provider[0]` instead of a blind `sync.data.provider[0]` (covered by fork-only `test/component/use-connected.test.ts`, not tracked here). | The hosted Zen provider self-registers (free models need no auth) and is inserted ahead of config-only providers, so with no config `model` pin and empty recents (a fresh `codevhub install` — the pin was dropped in PR #142 / hub PR #221) the TUI defaulted to "Big Pickle · OpenCode Zen" and would send messages to upstream's hosted gateway instead of the configured one. Same predicate as `app.tsx`'s startup gate and the footer indicator; when no provider qualifies the old first-provider behavior is kept. | Keep the predicate export and the `find` fallback; take upstream's surrounding changes to either file. If upstream reworks `fallbackModel`, re-apply the connected-first selection to its last-resort branch. | | `packages/tui/src/component/prompt/index.tsx` | The busy-state footer arm (`status().type !== "idle"`) also renders `props.hint` after the `esc interrupt` text, gated on `status().type !== "retry"` | The hidden-sidebar hint (see the `routes/session/index.tsx` hint row) only rendered in the footer `Switch`'s idle fallback arm, so it vanished for the whole duration of a running turn — exactly when todos are being created. Retry keeps its `space-between` error row uncluttered. | Keep the `{props.hint}` line in the busy `Match` arm (additive); take upstream's surrounding changes. | | `packages/tui/src/component/prompt/index.tsx` | Width-tiered busy footer: module-level `BUSY_FOOTER_PALETTE_MIN_WIDTH` (108) and `BUSY_FOOTER_HINT_MIN_WIDTH` (88) drive a `showPaletteHint` memo (`status().type === "idle" \|\| width >= 108`) wrapping the ` commands` text and a `showBusyHint` memo (`width >= 88`) added to the busy arm's `props.hint` gate | The busy row carries spinner + interrupt hint + sidebar hint + context/cost; as the terminal narrows flexbox runs the gaps to zero (cramped but intact) and below ~85 columns the row wraps to two lines and clips the cost. Shedding by value keeps it on one line down to 70: the static palette hint goes first, then the modified-file count; the spinner and interrupt hint never go. | Keep both constants, both memos and the two `Show` gates; take upstream's surrounding changes. Re-measure the thresholds if segments are added to the row. | +| `packages/tui/src/component/prompt/index.tsx` | The footer's right slot renders key hints and usage independently instead of upstream's either/or `Switch` (`usage()` else hints): module-level `FOOTER_HINTS_AND_USAGE_MIN_WIDTH` (120) drives a `usageMeta` memo (usage data, shown when `width >= 120` or `status().type !== "idle"`) and a `showKeyHints` memo (`!usageMeta() \|\| width >= 120`) replacing the inner `Switch`/`Match` pair with two sibling `Show` gates. Covered by `test/footer-width.view.test.tsx`. | Upstream shows the `shift+tab modes` / `tab agents` hints only while the session has no assistant output tokens, so one prompt permanently swaps them for the context/cost readout and the shortcuts stop being discoverable. A wide terminal fits both; narrower ones show the hints while idle (when you'd press them) and usage while busy (when it's changing). | Keep the constant, both memos and the two `Show` gates; take upstream's surrounding changes. Re-measure the threshold if segments are added to the row. | | `packages/tui/src/component/prompt/index.tsx` | Removed the editor open-file label from the footer's right-side group and lifted the state behind it out of `Prompt`: the local `dismissedEditorSelectionKey` signal and the `editorContext`/`editorPath`/`editorSelectionLabel`/`editorFileLabel`/`editorFileLabelDisplay`/`editorContextLabelState` memos are gone, replaced by `useEditorLabel()` (fork-only `src/context/editor-label.tsx`) — `selection()` feeds the `prompt.editor_context.clear` `enabled` predicate and the submit-time editor part, `state()` replaces `editor.labelState()` there, and `dismiss()` replaces the local `dismissEditorContext`. The pure helpers `hasEditorRangeSelection`/`getEditorRangeLabel` moved to that context module and are imported back for `formatEditorContext`. | The label now lives in the sidebar footer (see its row above). The dismissal state had to move with it: the sidebar must label the *effective* selection — the one this file actually attaches on submit — or it would advertise context the prompt has dismissed and won't send. | Keep the deletions, the `useEditorLabel` import + its three call sites, and the helper import from `../../context/editor-label`; take upstream's surrounding changes. If upstream reworks the editor-context label, apply the change to `editorContextFileLabel` in the fork-only sidebar module rather than restoring the footer render. | | `packages/tui/src/routes/session/index.tsx`, `packages/tui/src/component/prompt/index.tsx` | Left accent border removed entirely: the user-message block's `border={["left"]}`/`borderColor` (agent accent)/`customBorderChars` props and the prompt input box's two bordered boxes (main box with its `bottomLeft: "╹"` char, plus the height-1 `╹` terminator row) are replaced with `paddingLeft={1}`; the now-unused `borderHighlight` memo and the `tint`/`SplitBorder` imports are removed from the prompt (the `highlight`/`agentMetaAlpha` memos stay — they still color the agent name label); the fork's in-scrollbox banner box gains `paddingLeft={1}` so the banner/tips left edge lines up with the prompt/message blocks' background edge (the removed border glyph used to occupy that aligned column); `BlockTool` (expanded tool-output panels) likewise drops its `border={["left"]}`/`customBorderChars`/`borderColor` (the background-colored `┃` glyph over the panel background rendered as a stray vertical line on the panel's left edge) and bumps `paddingLeft` 2 → 3 to keep the content column | No vertical accent line next to sent messages, the prompt, or tool-output panels — removed rather than recolored to the background because background-colored glyphs render oddly on some terminals (e.g. PowerShell). `paddingLeft={1}` keeps the same 1-column offset, so text alignment with other transcript blocks is unchanged. | Keep our `paddingLeft={1}` boxes and the `borderHighlight`/`tint`/`SplitBorder` removals; take upstream's surrounding changes. If upstream adds new accent-colored left borders to these blocks, replace them with `paddingLeft={1}` the same way. | | `packages/tui/src/component/spinner.tsx` | `SPINNER_FRAMES` swapped from cli-spinners "dots" (10 braille frames) to cli-spinners "sand" (35 braille frames, falling-sand animation); interval stays 80 ms | Fork visual identity for the shared `Spinner` component (in-progress tool/thinking rows, startup loading, session-list dialogs). | Keep our frames array; take upstream's surrounding changes. | @@ -172,7 +173,7 @@ from the squash message first, or use a plain merge. | `packages/app/src/context/language.tsx`, `packages/app/src/entry.tsx`, `packages/desktop/src/renderer/i18n/index.ts`, `packages/console/app/src/i18n/index.ts`, `packages/console/app/src/lib/language.ts`, `packages/console/app/src/component/footer.tsx`, `packages/stats/app/src/i18n.ts`, `packages/enterprise/src/app.tsx`, `packages/app/e2e/regression/session-timeline-locale-projection.spec.ts` | Locale registries trimmed to `en` + `es`: `Locale` types, LOCALES/LABEL/TAG/DOCS maps, dict loaders, accept-language matchers; enterprise/entry `zh` special cases now use `es`; console footer always links Discord (Feishu was zh-only); e2e locale-projection spec tests `es` ("Explorado") instead of de/ar | Fork ships English + Spanish only. | Keep our trimmed lists; take upstream's structural changes and re-trim any new locale entries. | | `packages/tui/src/component/prompt/autocomplete.tsx`, `packages/app/src/components/prompt-input.tsx`, `packages/opencode/src/cli/cmd/run/footer.prompt.tsx`, `packages/core/src/filesystem/search.ts`, `packages/tui/test/fixture/tui-sdk.ts`, `packages/opencode/test/cli/run/footer.view.test.tsx`, `packages/core/test/filesystem/search.test.ts` | `@` mention autocomplete lists file/directory results before references/agents/MCP resources; TUI and run-CLI menus cap the leading files so up to 3 non-file matches stay in the first visible page; web app group rank reordered to recent → file → reference → agent → resource (extracted into fork-only `prompt-input/at-options.ts`). TUI and run-CLI file resources track menu visibility so opening on an empty query fetches (previously nothing refetched until the query changed); core's ripgrep-fallback `find` returns its first entries for an empty query to mirror fff. Tests: tui-sdk fixture's `FetchHandler` gains an optional `method` arg, `renderFooter` accepts `findFiles`/`agents` overrides + a mention-menu ordering test, core search tests cover the fallback's empty query (fork-only test files: `packages/tui/test/prompt-autocomplete.test.tsx`, `packages/app/test-browser/prompt-at-options.test.ts`, `packages/app/src/components/prompt-input/at-options{,.test}.ts`) | Files are the dominant `@` use case; they should rank first and appear on a bare `@`. | Keep our ordering/reserve logic, resource source tracking, empty-query branch, fixture/test additions; take upstream's surrounding changes. | | `packages/slack/.gitignore`, `sdks/vscode/.gitignore` | Added the missing trailing newline (content unchanged) | Upstream's copies end without a newline; POSIX text files should end with one, and tools that concatenate or append to these files misbehave without it. | Whitespace-only: keep the trailing newline; take upstream's entries. | -| `packages/tui/src/config/keybind.ts`, `packages/tui/src/component/prompt/index.tsx`, `packages/tui/src/app.tsx` | Permission-mode indicator + shift+tab cycle. keybind.ts: added `permission_mode_cycle: keybind("shift+tab", ...)` + its `permission.mode` CommandMap entry; `agent_cycle_reverse` default `"shift+tab"` → `"none"` (tab still cycles agents forward; previous-agent stays rebindable). prompt/index.tsx: the footer badge next to the agent name always renders in prompt mode — gray `manual` (`theme.textMuted`) in normal mode, red `auto` (`theme.error`) in auto mode (upstream only showed gray `auto`); the idle footer's shortcut hints gain a `shift+tab modes` text (via a `modeShortcut` `useCommandShortcut("permission.mode")` memo) left of `tab agents`, in the same no-usage `Match` arm. app.tsx: `"permission.mode"` added to `appBindingCommands` so the binding is gathered. | The permission mode is always visible (manual/auto) with auto in red as a danger signal, and shift+tab toggles it without opening the palette. | Keep the `permission_mode_cycle` definition + mapping, the `agent_cycle_reverse` `"none"` default, the badge `Show` block, the `modeShortcut` memo + hint text, and the `appBindingCommands` entry; take upstream's surrounding changes. If upstream reworks the auto badge or rebinds shift+tab, re-apply on top and re-audit the shift+tab conflict. | +| `packages/tui/src/config/keybind.ts`, `packages/tui/src/component/prompt/index.tsx`, `packages/tui/src/app.tsx` | Permission-mode indicator + shift+tab cycle. keybind.ts: added `permission_mode_cycle: keybind("shift+tab", ...)` + its `permission.mode` CommandMap entry; `agent_cycle_reverse` default `"shift+tab"` → `"none"` (tab still cycles agents forward; previous-agent stays rebindable). prompt/index.tsx: the footer badge next to the agent name always renders in prompt mode — gray `manual` (`theme.textMuted`) in normal mode, red `auto` (`theme.error`) in auto mode (upstream only showed gray `auto`); the footer's shortcut hints gain a `shift+tab modes` text (via a `modeShortcut` `useCommandShortcut("permission.mode")` memo) left of `tab agents`, in the same `Show` gate (see the hints-and-usage row above for when that gate renders). app.tsx: `"permission.mode"` added to `appBindingCommands` so the binding is gathered. | The permission mode is always visible (manual/auto) with auto in red as a danger signal, and shift+tab toggles it without opening the palette. | Keep the `permission_mode_cycle` definition + mapping, the `agent_cycle_reverse` `"none"` default, the badge `Show` block, the `modeShortcut` memo + hint text, and the `appBindingCommands` entry; take upstream's surrounding changes. If upstream reworks the auto badge or rebinds shift+tab, re-apply on top and re-audit the shift+tab conflict. | | `packages/core/src/filesystem/search.ts`, `packages/core/src/ripgrep/binary.ts`, `packages/core/src/filesystem.ts`, `packages/core/src/tool/read-filesystem.ts`, `packages/core/test/filesystem/search.test.ts`, `packages/core/test/location-filesystem.test.ts`, `packages/core/test/ripgrep-binary.test.ts`, `packages/core/test/preload.ts` | Windows file-search fallback robustness. search.ts: the background ripgrep index walk no longer dies silently (`Effect.orDie` in a forked fiber) — each attempt resets the accumulated file/directory state, failures log a warning ("file index scan failed, file search may be empty"), and the walk retries forever on a 1s exponential backoff capped at 30s spacing, so installing rg mid-session self-heals; directory entries are suffixed and detected with `"/"` instead of `path.sep` (both ripgrep and fff layers), and fallback `find` queries normalize `\` to `/` and collapse doubled slashes (directory tab-expansion appends `/` to an already-`/`-suffixed entry). binary.ts: `Effect.cached` on ripgrep binary resolution (which memoizes failure exits for the process lifetime) replaced with a success-only cache behind a semaphore, so a failed rg download (offline, blocked proxy) retries on the next use instead of bricking search until restart; the download tail tries GitHub first (each attempt bounded to 30s) and falls back to the CoDev landing-page mirror (`https://netmind.viettel.vn/codev/docs/code/downloads/rg---[.exe]`, a raw executable — no archive/extract — staged in a scoped temp dir then copied into place), via `Effect.catchCause` so a proxy block page (200 + HTML) that dies in `extract` as a defect still reaches the mirror. filesystem.ts `list` + read-filesystem.ts `list`: directory suffix `path.sep` → `"/"`; `fs.list` also `/`-normalizes `path.relative` output. Tests: search.test.ts pins `"/"`-suffixed directories, adds query-normalization coverage, and a stub-Ripgrep scan-retry test asserting the partial first walk is discarded; location-filesystem.test.ts pins `"src/"`; ripgrep-binary.test.ts adds mocked-HTTP coverage of the fallback order, success caching, and failure non-caching; preload.ts redirects `XDG_CACHE_HOME` to a fresh temp dir so no test can read or clobber the developer's real `~/.cache/codev`. | On Windows fff is disabled by default (upstream `e4300e9b74`), so the `@` mention popup and file search depend entirely on the ripgrep fallback; a missing `rg.exe` plus one failed download left the index permanently empty with no user-visible signal (the CI-workflows row's choco preinstall papered over the same swallowed failure). `\`-suffixed directory entries also broke `endsWith("/")` directory-mime detection in the run CLI and poisoned drill-down queries with backslashes that can never match the `/`-normalized index. | Keep our retry/reset scan, success-only binary cache, `"/"` suffixes, and query normalization; take upstream's surrounding changes. If upstream reworks the fallback or re-enables fff on win32, preserve the invariants: entry paths are `/`-normalized with `/`-suffixed directories on every platform, index-scan failures are logged and retried, a failed binary download is never cached, and the GitHub download keeps its CoDev-mirror fallback — the mirror filename scheme must match what codev-landing-page hosts under `public/docs/code/downloads` (and codevhub's `RG_VERSION` pin in its `src/lib/ripgrep.ts`); bump all three together when changing `VERSION`. | | `packages/tui/src/context/permission.tsx`, `packages/tui/src/app.tsx`, `packages/tui/src/ui/dialog-confirm.tsx`, `packages/tui/src/ui/toast.tsx` | Auto-approve consent gate. permission.tsx: mode always starts `"normal"` (upstream initialized it from `args.auto`); `toggle()` replaced by `set(mode)` plus a per-run `consented` flag + `consent()`. app.tsx: shared `enableAutoApprove()` (title in the `AUTO_APPROVE_TITLE` module const, body from an `autoApproveMessage` accessor, and a shorter one-line `autoApproveToast` accessor for the toast, which only has a few seconds on screen; both redden the word "dangerous" with `theme.error` and are passed unevaluated because renderables can only be created during render) shows a `DialogConfirm` opened on cancel and, on confirm, a warning toast under the same title, then sets the mode; both entry points funnel through it — an `onMount` when `args.auto` is set, and the `permission.mode` command (shift+tab / palette), which enables via the gate and disables directly. Consent is asked once per run; later toggles are silent. dialog-confirm.tsx: optional `defaultChoice` prop + 5th `DialogConfirm.show` arg selecting the initially-active button (omitted keeps upstream's confirm-first default); `message` widened to `JSX.Element \| (() => JSX.Element)` and called at the render site. toast.tsx: same `message` widening, resolved by a `message()` helper in `Toast()` (fork-only test file: `packages/tui/test/auto-approve-consent.view.test.tsx`). | Auto-approve is dangerous, so `--auto` (and its hidden aliases) and the first shift+tab into auto require explicit confirmation; declining or dismissing the dialog stays in manual mode, opening on cancel keeps a stray return from granting it, and the risk word is red like the `auto` badge. | Keep the consent flag + gate flow, the `toggle()` removal, the `defaultChoice` prop, and the accessor-aware `message` on both dialog and toast; take upstream's surrounding changes. If upstream adds new writers of the permission mode, route their enable path through `enableAutoApprove()`. | diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index a0fd42a41a..9b6787b15b 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -73,6 +73,13 @@ const FOOTER_FRAMES = ["▰▱▱▱▱▱▱", "▰▰▱▱▱▱▱", "▰▰ const BUSY_FOOTER_PALETTE_MIN_WIDTH = 108 const BUSY_FOOTER_HINT_MIN_WIDTH = 88 +// The right footer slot carries the shift+tab/tab key hints and, once the +// session has token data, the context/cost readout. A wide terminal fits both; +// below this the slot shows the hints while idle and usage while busy, so the +// shortcuts stay discoverable after the first response instead of vanishing +// for the rest of the session. +const FOOTER_HINTS_AND_USAGE_MIN_WIDTH = 120 + export type PromptProps = { sessionID?: string visible?: boolean @@ -1342,6 +1349,14 @@ export function Prompt(props: PromptProps) { () => status().type === "idle" || dimensions().width >= BUSY_FOOTER_PALETTE_MIN_WIDTH, ) const showBusyHint = createMemo(() => dimensions().width >= BUSY_FOOTER_HINT_MIN_WIDTH) + const usageMeta = createMemo(() => { + const item = usage() + if (!item) return + if (dimensions().width >= FOOTER_HINTS_AND_USAGE_MIN_WIDTH) return item + if (status().type !== "idle") return item + return undefined + }) + const showKeyHints = createMemo(() => !usageMeta() || dimensions().width >= FOOTER_HINTS_AND_USAGE_MIN_WIDTH) const maxHeight = createMemo(() => tuiConfig.prompt?.max_height ?? Math.max(6, Math.floor(dimensions().height / 3))) const moveLabelWidth = createMemo(() => Math.max(12, Math.min(44, dimensions().width - 48))) @@ -1659,23 +1674,21 @@ export function Prompt(props: PromptProps) { - - - {(item) => ( - - {[item().context, item().cost].filter(Boolean).join(" · ")} - - )} - - - - {modeShortcut()} modes + + {(item) => ( + + {[item().context, item().cost].filter(Boolean).join(" · ")} - - {agentShortcut()} agents - - - + )} + + + + {modeShortcut()} modes + + + {agentShortcut()} agents + + {paletteShortcut()} commands diff --git a/packages/tui/test/footer-width.view.test.tsx b/packages/tui/test/footer-width.view.test.tsx index c6cc999840..031c2477d0 100644 --- a/packages/tui/test/footer-width.view.test.tsx +++ b/packages/tui/test/footer-width.view.test.tsx @@ -39,8 +39,10 @@ const diffs = Array.from({ length: 12 }, (_, index) => ({ // The busy footer carries spinner + interrupt hint + modified-file count + // context/cost. It sheds the palette hint below 108 columns and the file count -// below 88 so the row never wraps; the interrupt hint always survives. -async function busyFooterRow(width: number) { +// below 88 so the row never wraps; the interrupt hint always survives. The +// right slot fits both the key hints and context/cost from 120 columns up; +// narrower it shows the hints while idle and usage while busy. +async function footerRow(width: number, opts?: { idle?: boolean }) { const setup = await createTestRenderer({ width, height: 24, useThread: false }) const core = await import("@opentui/core") mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer })) @@ -122,21 +124,25 @@ async function busyFooterRow(width: number) { ) await ready await waitForFrame(setup, (frame) => frame.includes("Baked for")) - const busy: GlobalEvent = { - directory, - project: "proj_test", - payload: { - id: "evt_busy", - type: "session.status", - properties: { sessionID: session.id, status: { type: "busy" } }, - }, + if (!opts?.idle) { + const busy: GlobalEvent = { + directory, + project: "proj_test", + payload: { + id: "evt_busy", + type: "session.status", + properties: { sessionID: session.id, status: { type: "busy" } }, + }, + } + events.emit(busy) } - events.emit(busy) - const frame = await waitForFrame(setup, (f) => f.includes("interrupt")) + // Idle rows anchor on the always-shown "tab agents" hint instead. + const marker = opts?.idle ? "agents" : "interrupt" + const frame = await waitForFrame(setup, (f) => f.includes(marker)) const rows = frame.split("\n").map((line) => line.replace(/\s+$/, "")) process.emit("SIGHUP") await task - return rows[rows.findLastIndex((line) => line.includes("interrupt"))] + return rows[rows.findLastIndex((line) => line.includes(marker))] } finally { if (!setup.renderer.isDestroyed) setup.renderer.destroy() mock.restore() @@ -144,27 +150,52 @@ async function busyFooterRow(width: number) { } test("busy footer keeps every hint on a wide terminal", async () => { - const row = await busyFooterRow(120) + const row = await footerRow(120) expect(row).toContain("interrupt") expect(row).toContain("modified files") expect(row).toContain("commands") + // Wide enough for the key hints and the usage readout together. + expect(row).toContain("agents") + expect(row).toContain("modes") + expect(row).toContain("$123.45") expect(row.length).toBeLessThanOrEqual(120) }, 30_000) test("busy footer sheds the palette hint at 100 columns", async () => { - const row = await busyFooterRow(100) + const row = await footerRow(100) expect(row).toContain("interrupt") expect(row).toContain("modified files") expect(row).not.toContain("commands") + // Narrow + busy: usage wins the right slot, the key hints go. + expect(row).not.toContain("agents") + expect(row).toContain("$123.45") expect(row.length).toBeLessThanOrEqual(100) }, 30_000) test("busy footer sheds the modified-file count at 80 columns", async () => { - const row = await busyFooterRow(80) + const row = await footerRow(80) expect(row).toContain("interrupt") expect(row).not.toContain("modified files") expect(row).not.toContain("commands") + expect(row).not.toContain("agents") // Still one line: the row that carries the interrupt hint also carries usage. expect(row).toContain("$123.45") expect(row.length).toBeLessThanOrEqual(80) }, 30_000) + +test("idle footer shows key hints and usage on a wide terminal", async () => { + const row = await footerRow(120, { idle: true }) + expect(row).toContain("modes") + expect(row).toContain("agents") + expect(row).toContain("commands") + expect(row).toContain("$123.45") + expect(row.length).toBeLessThanOrEqual(120) +}, 30_000) + +test("idle footer keeps key hints over usage at 80 columns", async () => { + const row = await footerRow(80, { idle: true }) + expect(row).toContain("modes") + expect(row).toContain("agents") + expect(row).not.toContain("$123.45") + expect(row.length).toBeLessThanOrEqual(80) +}, 30_000) From 85dea3d6551b2165a4a73f2474c636cf607a4ceb Mon Sep 17 00:00:00 2001 From: minhn4 Date: Sun, 2 Aug 2026 19:03:21 +0700 Subject: [PATCH 2/2] minor changes --- CHANGELOG.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4bbe351ae..4c7110ba4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,6 @@ file is the human-readable summary of what changed and why. ### TUI -- **The `shift+tab modes` / `tab agents` hints survive the first response** — - previously the prompt footer's right slot swapped them for the context/cost - readout as soon as the session had any assistant output, and they never came - back. From 120 columns up the slot now shows the key hints and the usage - readout side by side; on narrower terminals it shows the hints while the - session is idle and the usage readout while it is busy. - ## 1.18.4-5 ### TUI