From cc135df03bfb293db175699ccd6a50d4174d1bcd Mon Sep 17 00:00:00 2001 From: minhn4 Date: Thu, 30 Jul 2026 13:50:02 +0700 Subject: [PATCH] feat(tui): dedup agent/model trailer on last message The trailer under the last assistant reply repeated the agent and model the prompt footer already shows one line below. When they match the current selection, hide the marker and agent/model text and render the elapsed time as "Baked for " instead; skip the line entirely when nothing is left to show. Older messages and mismatched selections keep the full trailer. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 8 +++ DIVERGENCES.md | 1 + packages/tui/src/routes/session/index.tsx | 64 +++++++++++++++-------- 3 files changed, 51 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 267807bdbc..09e91443de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ file is the human-readable summary of what changed and why. ### TUI +- **No more duplicated agent · model line** — the trailer under the last + assistant reply (`▣ Build · MiniMax/MiniMax-M2.7 · 3m`) used to repeat the + agent and model the prompt footer already shows one line below. When they + match the current selection, the `▣` marker and the agent/model text are now + hidden and only the elapsed time remains, shown as `Baked for 3m`. Older + messages in the transcript, and the last message after you switch agent or + model, keep the full trailer. + - **Auto-approve now asks for consent** — launching with `--auto` (or its hidden aliases) no longer enables auto-approve silently: a confirm dialog warns that permission requests not explicitly denied will be approved diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 4309b5bc9c..ccdfc1e5c6 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -98,6 +98,7 @@ from the squash message first, or use a plain merge. | `packages/tui/src/routes/session/index.tsx` | Flow transcript: the scrollbox opens with a home-like banner (`Logo` + `home_bottom` tips slot, `placement="start"`) as its first entry for root sessions (subagent sessions keep a plain 1-row pad), its `maxHeight` is clamped to the content height (a `transcriptHeight` signal read off the content's yoga node in the renderer root's `layout-changed` handler, which then re-runs `calculateLayout` so the clamp lands in the same frame — `scrollHeight` is a frame stale there, and painting with a stale clamp flickers during streaming) so the `Prompt` rides directly below the last message, and a trailing flexGrow spacer absorbs the leftover space until content fills the screen (then the prompt is effectively bottom-anchored and the banner scrolls away with history); the session `Prompt` gets home's `placeholder` pool when the session has no messages so the input shows an `Ask anything... ""` placeholder like home | Startup lands in an empty session instead of the home screen; the transcript flows top-down like terminal scrollback — logo top-left, input following the conversation down — instead of a bottom-anchored prompt with a void above it. | Keep our banner block inside the scrollbox, the `transcriptHeight` clamp + `layout-changed` listener, the trailing spacer, and the `placeholders` prop; take upstream's surrounding changes. | | `packages/tui/src/routes/session/index.tsx` | Sidebar visibility is session-local: removed upstream's persisted `kv.signal<"auto" \| "hide">("sidebar", "auto")` (and the `sidebar() === "auto" && wide()` branch of `sidebarVisible`); visibility is driven only by the non-persisted `sidebarOpen` signal, which the `session.sidebar.toggle` command flips (supersedes the earlier `"auto"` → `"hide"` default change) | The right sidebar starts hidden in every session regardless of terminal width or what the user did last session; `ctrl+b`/`b` only affects the current session. A stale `sidebar` key in existing kv.json files is simply never read. | Keep the kv signal removed and `sidebarVisible` reading only `parentID` + `sidebarOpen`; take upstream's surrounding changes. | | `packages/tui/src/routes/session/index.tsx` | Sidebar hint in the prompt footer: a `sidebarHint` memo (active-todo count from `sync.data.todo`, modified-file count from `sync.data.session_diff`, only when the sidebar is hidden and the session isn't a subagent; the label text comes from `sidebarHintLabel` in the fork-only `routes/session/sidebar-hint.ts`) plus a `sidebarShortcut` (`useCommandShortcut("session.sidebar.toggle")`), passed as the `Prompt`'s previously unused `hint` prop — e.g. `ctrl+b 3 todos · 5 files` | The sidebar starts hidden in this fork (see the row above), so users would otherwise never learn there are todos/modified files or how to reveal them. | Keep our `sidebarShortcut`/`sidebarHint` memos, the `sidebar-hint` import, and the `hint={…}` prop on `` (all additive); take upstream's surrounding changes. If upstream starts passing its own `hint` to this ``, merge the two under one `Show`. | +| `packages/tui/src/routes/session/index.tsx` | Assistant-message trailer dedup: a `suppressed` memo in `AssistantMessage` hides the `▣ Agent · Model` prefix on the last message when it matches the currently selected agent (`local.agent.current()?.name === message.mode`) and model (`local.model.current()` vs the message's `providerID`/`modelID`), since the prompt footer right below already shows that selection; the duration still renders as `Baked for ` (and `interrupted` keeps rendering on aborts), and the trailer box is skipped entirely when suppression leaves nothing to show | The last message's trailer and the prompt footer showed the same agent + model twice, one line apart. | Keep the `suppressed` memo and the `Show` gates around the trailer spans; take upstream's surrounding trailer changes (new suffixes should go inside the existing `` with a `·` separator like `interrupted`). | | `packages/plugin/src/tui.ts` | `home_bottom` slot props widened from `{}` to `{ placement?: "center" \| "start" }` | The empty-session banner asks `home_bottom` contributors to left-align; optional prop keeps the home route and third-party plugins (no prop → centered) unchanged. | Keep our `placement` prop; take upstream's other slot-map changes. | | `packages/tui/src/feature-plugins/home/tips.tsx` | `home_bottom` slot fn reads the new `placement` prop; with `"start"` the tips box left-aligns (`flex-start`) and drops its `paddingTop` 3 | The top-left session banner needs the tip flush under the logo; the centered home route keeps upstream spacing. | Keep our `placement` threading + conditional alignItems/paddingTop; take upstream's surrounding changes. | | `packages/tui/src/feature-plugins/system/which-key.tsx` | `HomeHint` reads the new `placement` prop from the `home_bottom` slot; with `"start"` it left-aligns | Same top-left banner alignment as the tips row above. | Keep our `placement` threading + conditional alignItems; take upstream's surrounding changes. | diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index b93aacc5a2..ac9c3360ea 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -1549,6 +1549,18 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las return props.message.time.completed - user.time.created }) + // the prompt footer already shows the current agent and model, so hide the + // trailer's copy on the last message when it would repeat that selection + const suppressed = createMemo(() => { + if (!props.last) return false + const currentModel = local.model.current() + return ( + local.agent.current()?.name === props.message.mode && + currentModel?.providerID === props.message.providerID && + currentModel?.modelID === props.message.modelID + ) + }) + const childShortcut = useCommandShortcut("session.child.first") const backgroundShortcut = useCommandShortcut("session.background") @@ -1610,28 +1622,36 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las - alwaysSeparate.add(el)} paddingLeft={3}> - - - ▣{" "} - {" "} - {Locale.titlecase(props.message.mode)} - · {model()} - - · {Locale.duration(duration())} - - - · interrupted - - - + + alwaysSeparate.add(el)} paddingLeft={3}> + + + + ▣{" "} + {" "} + {Locale.titlecase(props.message.mode)} + · {model()} + + + + {suppressed() ? `Baked for ${Locale.duration(duration())}` : ` · ${Locale.duration(duration())}`} + + + + + {suppressed() && !duration() ? "interrupted" : " · interrupted"} + + + + +