From 08ea9cc31d8e17a1101fde0f366fc5050eef0c03 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 1 Aug 2026 15:39:55 -0700 Subject: [PATCH] fix(tui): keep session modes scoped on switch (#117658) * fix(tui): keep session footer modes truthful * test(tui): record footer behavior evidence --- src/tui/tui-backend.ts | 1 + src/tui/tui-formatters.test.ts | 121 +++++++++++++----- src/tui/tui-formatters.ts | 40 +++++- .../tui-pty-harness-fixture-test-support.ts | 22 +++- src/tui/tui-pty-harness.e2e.test.ts | 74 ++++++++++- src/tui/tui-session-actions.test.ts | 58 +++++++++ src/tui/tui-session-actions.ts | 50 ++------ src/tui/tui-session-info.ts | 51 ++++++++ src/tui/tui.ts | 46 ++----- 9 files changed, 342 insertions(+), 121 deletions(-) create mode 100644 src/tui/tui-session-info.ts diff --git a/src/tui/tui-backend.ts b/src/tui/tui-backend.ts index 1df715d7fdee..e732b03ddc2e 100644 --- a/src/tui/tui-backend.ts +++ b/src/tui/tui-backend.ts @@ -87,6 +87,7 @@ export type TuiSessionList = { | "thinkingLevels" | "fastMode" | "verboseLevel" + | "traceLevel" | "reasoningLevel" | "model" | "contextTokens" diff --git a/src/tui/tui-formatters.test.ts b/src/tui/tui-formatters.test.ts index ac53f87c4b85..7161e31eab7f 100644 --- a/src/tui/tui-formatters.test.ts +++ b/src/tui/tui-formatters.test.ts @@ -1,4 +1,5 @@ // Covers formatting helpers used by TUI status and message rendering. +import { Text, visibleWidth } from "@earendil-works/pi-tui"; import { describe, expect, it } from "vitest"; import { markInboundContextLabel } from "../auto-reply/reply/inbound-context-marker.js"; import { MALFORMED_STREAMING_FRAGMENT_ERROR_MESSAGE } from "../shared/assistant-error-format.js"; @@ -6,56 +7,106 @@ import { extractContentFromMessage, extractTextFromMessage, extractThinkingFromMessage, - formatModelFooter, - formatGoalFooter, + formatTuiFooter, formatTuiErrorMessage, isCommandMessage, sanitizeRenderableText, } from "./tui-formatters.js"; -describe("formatModelFooter", () => { - it("shows a compact model name and its active thinking level", () => { +describe("formatTuiFooter", () => { + it("shows session modes and the process delivery mode in one compact summary", () => { expect( - formatModelFooter({ - model: "gpt-5.6-sol@openai:setup-64cddea3-938c-431e-be3b-aa47090577c7", + formatTuiFooter({ + agentLabel: "Main", + sessionLabel: "work", + sessionInfo: { + model: "gpt-5.6-sol@openai:setup-64cddea3-938c-431e-be3b-aa47090577c7", + fastMode: "auto", + verboseLevel: "full", + traceLevel: "raw", + reasoningLevel: "stream", + totalTokens: 1_200, + contextTokens: 128_000, + }, thinkingLevel: "high", + deliver: true, }), - ).toBe("gpt-5.6-sol high"); + ).toBe( + "agent Main | session work | gpt-5.6-sol high | fast:auto | verbose full | trace:raw | reasoning:stream | deliver:on | tokens 1.2k/128k (1%)", + ); }); -}); -describe("formatGoalFooter", () => { - it("renders active goal usage", () => { + it("keeps disabled session modes hidden while reporting disabled delivery", () => { expect( - formatGoalFooter({ - schemaVersion: 1, - id: "goal-1", - objective: "land PR", - status: "active", - createdAt: 1, - updatedAt: 1, - tokenStart: 0, - tokensUsed: 12_000, - tokenBudget: 30_000, - continuationTurns: 0, + formatTuiFooter({ + agentLabel: "Main", + sessionLabel: "main", + sessionInfo: { model: "fixture-model" }, + deliver: false, }), - ).toBe("Pursuing goal (12k/30k)"); + ).toBe("agent Main | session main | fixture-model | deliver:off | tokens ?"); + }); + + it("wraps the compact summary within the terminal width", () => { + const summary = formatTuiFooter({ + agentLabel: "Main", + sessionLabel: "a-long-session-name", + sessionInfo: { + model: "fixture-provider/a-long-model-name", + traceLevel: "raw", + reasoningLevel: "stream", + }, + deliver: true, + }); + + expect(new Text(summary, 1, 0).render(48).every((line) => visibleWidth(line) <= 48)).toBe(true); + }); + + it("renders active goal usage", () => { + const footer = formatTuiFooter({ + agentLabel: "Main", + sessionLabel: "main", + sessionInfo: { + goal: { + schemaVersion: 1, + id: "goal-1", + objective: "land PR", + status: "active", + createdAt: 1, + updatedAt: 1, + tokenStart: 0, + tokensUsed: 12_000, + tokenBudget: 30_000, + continuationTurns: 0, + }, + }, + deliver: false, + }); + + expect(footer).toContain("Pursuing goal (12k/30k)"); }); it("renders resumable blocked goals", () => { - expect( - formatGoalFooter({ - schemaVersion: 1, - id: "goal-1", - objective: "land PR", - status: "blocked", - createdAt: 1, - updatedAt: 1, - tokenStart: 0, - tokensUsed: 0, - continuationTurns: 0, - }), - ).toBe("Goal blocked (/goal resume)"); + const footer = formatTuiFooter({ + agentLabel: "Main", + sessionLabel: "main", + sessionInfo: { + goal: { + schemaVersion: 1, + id: "goal-1", + objective: "land PR", + status: "blocked", + createdAt: 1, + updatedAt: 1, + tokenStart: 0, + tokensUsed: 0, + continuationTurns: 0, + }, + }, + deliver: false, + }); + + expect(footer).toContain("Goal blocked (/goal resume)"); }); }); diff --git a/src/tui/tui-formatters.ts b/src/tui/tui-formatters.ts index 5156530c1e28..ac059c3d1011 100644 --- a/src/tui/tui-formatters.ts +++ b/src/tui/tui-formatters.ts @@ -10,6 +10,7 @@ import { formatRawAssistantErrorForUi } from "../shared/assistant-error-format.j import { extractAssistantVisibleText } from "../shared/chat-message-content.js"; import { chunkTextByBreakResolver } from "../shared/text-chunking.js"; import { formatTokenCount } from "../utils/usage-format.js"; +import type { SessionInfo } from "./tui-types.js"; const REPLACEMENT_CHAR_RE = /\uFFFD/g; const MAX_TOKEN_CHARS = 32; @@ -35,7 +36,7 @@ const FENCED_CODE_RE = /(```|~~~)[^\n]*\n[\s\S]*?\n\1[^\n]*/g; const INLINE_CODE_RE = /(`+)(?:(?!\1).)+?\1/g; /** Keep routing/provider/profile details in session state, not the compact footer. */ -export function formatModelFooter(params: { +function formatModelFooter(params: { model?: string | null; thinkingLevel?: string | null; }): string { @@ -44,6 +45,39 @@ export function formatModelFooter(params: { return thinkingLevel && thinkingLevel !== "off" ? `${model} ${thinkingLevel}` : model; } +/** Format the compact TUI footer from authoritative session and process state. */ +export function formatTuiFooter(params: { + agentLabel: string; + sessionLabel: string; + sessionInfo: SessionInfo; + thinkingLevel?: string | null; + deliver: boolean; +}): string { + const { sessionInfo } = params; + const fastLabel = + sessionInfo.fastMode === "auto" ? "fast:auto" : sessionInfo.fastMode === true ? "fast" : null; + const verbose = sessionInfo.verboseLevel ?? "off"; + const trace = sessionInfo.traceLevel ?? "off"; + const reasoning = sessionInfo.reasoningLevel ?? "off"; + const traceLabel = trace === "raw" ? "trace:raw" : trace === "on" ? "trace" : null; + const reasoningLabel = + reasoning === "on" ? "reasoning" : reasoning === "stream" ? "reasoning:stream" : null; + return [ + `agent ${params.agentLabel}`, + `session ${params.sessionLabel}`, + formatModelFooter({ model: sessionInfo.model, thinkingLevel: params.thinkingLevel }), + formatGoalFooter(sessionInfo.goal), + fastLabel, + verbose !== "off" ? `verbose ${verbose}` : null, + traceLabel, + reasoningLabel, + `deliver:${params.deliver ? "on" : "off"}`, + formatTokens(sessionInfo.totalTokens ?? null, sessionInfo.contextTokens ?? null), + ] + .filter(Boolean) + .join(" | "); +} + function hasControlChars(text: string): boolean { for (const char of text) { const code = char.charCodeAt(0); @@ -615,7 +649,7 @@ export function isCommandMessage(message: unknown): boolean { return (message as Record).command === true; } -export function formatTokens(total?: number | null, context?: number | null) { +function formatTokens(total?: number | null, context?: number | null) { if (total == null && context == null) { return "tokens ?"; } @@ -637,7 +671,7 @@ function formatGoalUsage(goal: SessionGoal): string | null { return `${formatTokenCount(goal.tokensUsed)}/${formatTokenCount(goal.tokenBudget)}`; } -export function formatGoalFooter(goal?: SessionGoal): string | null { +function formatGoalFooter(goal?: SessionGoal): string | null { if (!goal) { return null; } diff --git a/src/tui/tui-pty-harness-fixture-test-support.ts b/src/tui/tui-pty-harness-fixture-test-support.ts index 88114fe65746..56f2459eee66 100644 --- a/src/tui/tui-pty-harness-fixture-test-support.ts +++ b/src/tui/tui-pty-harness-fixture-test-support.ts @@ -37,6 +37,7 @@ export async function writeTuiPtyFixtureScript(dir: string) { const footerModel = process.env.OPENCLAW_TUI_PTY_MODEL; const footerThinkingLevel = process.env.OPENCLAW_TUI_PTY_THINKING_LEVEL; let verboseLevel = process.env.OPENCLAW_TUI_PTY_VERBOSE_LEVEL; + let modeTargetTraceLevel: string | undefined; const launchThinkingLevel = process.env.OPENCLAW_TUI_PTY_LAUNCH_THINKING; const initialMessage = process.env.OPENCLAW_TUI_PTY_INITIAL_MESSAGE; const enablePickerFixture = process.env.OPENCLAW_TUI_PTY_PICKER_FIXTURE === "1"; @@ -76,15 +77,23 @@ export async function writeTuiPtyFixtureScript(dir: string) { } function sessionEntry(key = "main") { + const isModeSource = key.endsWith(":mode-source"); + const isModeTarget = key.endsWith(":mode-target"); + const entryFastMode = isModeSource ? true : isModeTarget ? undefined : fastMode; + const entryVerboseLevel = isModeSource ? "full" : isModeTarget ? undefined : verboseLevel; + const entryTraceLevel = isModeSource ? "raw" : isModeTarget ? modeTargetTraceLevel : undefined; + const entryReasoningLevel = isModeSource ? "stream" : undefined; return { key, displayName: "Main", model: currentModel, modelProvider: "fixture-provider", contextTokens: 128, - fastMode, + ...(entryFastMode !== undefined ? { fastMode: entryFastMode } : {}), ...(currentThinkingLevel ? { thinkingLevel: currentThinkingLevel } : {}), - ...(verboseLevel ? { verboseLevel } : {}), + ...(entryVerboseLevel ? { verboseLevel: entryVerboseLevel } : {}), + ...(entryTraceLevel ? { traceLevel: entryTraceLevel } : {}), + ...(entryReasoningLevel ? { reasoningLevel: entryReasoningLevel } : {}), thinkingLevels: [], }; } @@ -399,10 +408,12 @@ export async function writeTuiPtyFixtureScript(dir: string) { messages: [{ role: "user", content: rapidSwitchMarker + "_HISTORY_MARKER" }], }; } + const includeSessionInfo = + Boolean(footerModel) || sessionKey.endsWith(":mode-source") || sessionKey.endsWith(":mode-target"); return { messages: [], fastMode, - ...(footerModel + ...(includeSessionInfo ? { thinkingLevel: footerThinkingLevel, sessionInfo: sessionEntry(sessionKey), @@ -455,6 +466,9 @@ export async function writeTuiPtyFixtureScript(dir: string) { if (typeof opts.verboseLevel === "string") { verboseLevel = opts.verboseLevel; } + if (typeof opts.traceLevel === "string" && opts.key.endsWith(":mode-target")) { + modeTargetTraceLevel = opts.traceLevel; + } return { ok: true, path: "", @@ -554,7 +568,7 @@ export async function writeTuiPtyFixtureScript(dir: string) { }, session: { scope: "per-sender", mainKey: "main" }, }, - deliver: false, + deliver: process.env.OPENCLAW_TUI_PTY_DELIVER === "1", thinking: launchThinkingLevel, message: initialMessage, historyLimit: 5, diff --git a/src/tui/tui-pty-harness.e2e.test.ts b/src/tui/tui-pty-harness.e2e.test.ts index ff1b7c02a226..cde0d2103313 100644 --- a/src/tui/tui-pty-harness.e2e.test.ts +++ b/src/tui/tui-pty-harness.e2e.test.ts @@ -131,10 +131,76 @@ describe.sequential("TUI PTY harness", () => { STARTUP_TEST_TIMEOUT_MS, ); + it( + "keeps session modes scoped while trace changes and delivery stays process-owned", + async () => { + const modeFixture = await startTuiFixture({ + env: { + OPENCLAW_TUI_PTY_DELIVER: "1", + OPENCLAW_TUI_PTY_MODEL: "fixture-model", + }, + }); + try { + await modeFixture.run.waitForOutput("deliver:on", STARTUP_TIMEOUT_MS); + await modeFixture.run.write("/session agent:main:mode-source\r", { delay: false }); + await modeFixture.waitForLogEntry( + (entry) => + entry.method === "loadHistory" && + objectFieldEquals(entry, "sessionKey", "agent:main:mode-source"), + ); + await modeFixture.run.waitForOutput( + "trace:raw | reasoning:stream | deliver:on", + STARTUP_TIMEOUT_MS, + ); + + const targetOutputOffset = modeFixture.run.visibleOutput().length; + await modeFixture.run.write("/session agent:main:mode-target\r", { delay: false }); + await modeFixture.waitForLogEntry( + (entry) => + entry.method === "loadHistory" && + objectFieldEquals(entry, "sessionKey", "agent:main:mode-target"), + ); + await modeFixture.run.waitForOutput("session mode-target", STARTUP_TIMEOUT_MS); + const targetOutput = modeFixture.run.visibleOutput().slice(targetOutputOffset); + expect(targetOutput).toContain("deliver:on"); + expect(targetOutput).not.toContain("fast:auto"); + expect(targetOutput).not.toContain("verbose full"); + expect(targetOutput).not.toContain("trace:raw"); + expect(targetOutput).not.toContain("reasoning:stream"); + + await modeFixture.run.write("/trace on\r", { delay: false }); + await modeFixture.waitForLogEntry( + (entry) => + entry.method === "patchSession" && objectFieldEquals(entry, "traceLevel", "on"), + ); + await modeFixture.run.waitForOutput("trace | deliver:on", STARTUP_TIMEOUT_MS); + + await modeFixture.run.write("delivery proof\r", { delay: false }); + const sent = await modeFixture.waitForLogEntry( + (entry) => + entry.method === "sendChat" && objectFieldEquals(entry, "message", "delivery proof"), + ); + expect(sent.payload).toMatchObject({ deliver: true }); + console.log( + `[behavior-evidence] tui-session-footer ${JSON.stringify({ + terminal: "real PTY", + sourceModesVisible: true, + targetModesCleared: true, + traceTransitionVisible: true, + fixedDeliveryPropagated: true, + })}`, + ); + } finally { + await modeFixture.cleanup(); + } + }, + STARTUP_TEST_TIMEOUT_MS, + ); + it( "keeps the launch thinking override active across session-level changes", async () => { - const footerNeedle = "fixture-provider/fixture-model high | tokens"; + const footerNeedle = "fixture-provider/fixture-model high | deliver:off | tokens"; await thinkingOverrideFixture.run.waitForOutput(footerNeedle, STARTUP_TIMEOUT_MS); await thinkingOverrideFixture.run.waitForOutput( "PTY_RESPONSE: thinking override proof", @@ -167,9 +233,11 @@ describe.sequential("TUI PTY harness", () => { .visibleOutput() .slice(sessionChangeOutputOffset); expect(outputAfterSessionChange).toContain(footerNeedle); - expect(outputAfterSessionChange).not.toContain("fixture-provider/fixture-model low | tokens"); expect(outputAfterSessionChange).not.toContain( - "fixture-provider/fixture-model medium | tokens", + "fixture-provider/fixture-model low | deliver:off | tokens", + ); + expect(outputAfterSessionChange).not.toContain( + "fixture-provider/fixture-model medium | deliver:off | tokens", ); }, STARTUP_TEST_TIMEOUT_MS, diff --git a/src/tui/tui-session-actions.test.ts b/src/tui/tui-session-actions.test.ts index f68ebf0c2da3..b095ce86731c 100644 --- a/src/tui/tui-session-actions.test.ts +++ b/src/tui/tui-session-actions.test.ts @@ -1264,6 +1264,64 @@ describe("tui session actions", () => { expect(setActivityStatus).toHaveBeenLastCalledWith("idle"); }); + it("replaces session-scoped modes when switching to a session without overrides", async () => { + const state = createBaseState({ + currentSessionKey: "agent:main:source", + sessionInfo: { + fastMode: true, + verboseLevel: "full", + traceLevel: "raw", + reasoningLevel: "stream", + }, + }); + const loadHistory = vi.fn().mockResolvedValue({ + sessionInfo: { + key: "agent:main:target", + sessionId: "session-target", + }, + messages: [], + }); + const { setSession } = createTestSessionActions({ + client: { listSessions: vi.fn(), loadHistory } as unknown as TuiBackend, + state, + }); + + await setSession("agent:main:target"); + + expect(state.sessionInfo).toMatchObject({ + fastMode: undefined, + verboseLevel: undefined, + traceLevel: undefined, + reasoningLevel: undefined, + }); + }); + + it("merges a same-session mode patch without clearing untouched modes", () => { + const state = createBaseState({ + sessionInfo: { + fastMode: true, + verboseLevel: "full", + traceLevel: "off", + reasoningLevel: "stream", + }, + }); + const { applySessionInfoFromPatch } = createTestSessionActions({ state }); + + applySessionInfoFromPatch({ + ok: true, + path: "/sessions/patch", + key: "agent:main:main", + entry: { traceLevel: "raw" }, + }); + + expect(state.sessionInfo).toMatchObject({ + fastMode: true, + verboseLevel: "full", + traceLevel: "raw", + reasoningLevel: "stream", + }); + }); + it("keeps the newer session when an earlier switch's history resolves last", async () => { const historyA = createDeferred(); const historyB = createDeferred(); diff --git a/src/tui/tui-session-actions.ts b/src/tui/tui-session-actions.ts index 20b8a6585bfa..4cf14f7d54ee 100644 --- a/src/tui/tui-session-actions.ts +++ b/src/tui/tui-session-actions.ts @@ -19,6 +19,12 @@ import { isCommandMessage, } from "./tui-formatters.js"; import { readTuiSessionUserMessage } from "./tui-session-events.js"; +import { + clearTuiSessionModeOverrides, + sessionInfoUiEquals, + type SessionInfoDefaults, + type SessionInfoEntry, +} from "./tui-session-info.js"; import { TUI_SESSION_LOOKUP_LIMIT } from "./tui-session-list-policy.js"; import { getTuiSessionProjection, @@ -26,7 +32,7 @@ import { reduceTuiSessionProjection, } from "./tui-session-projection.js"; import * as submit from "./tui-submit-state.js"; -import type { SessionInfo, TuiHistoryLoadResult, TuiOptions, TuiStateAccess } from "./tui-types.js"; +import type { TuiHistoryLoadResult, TuiOptions, TuiStateAccess } from "./tui-types.js"; type SessionActionBtwPresenter = { clear: () => void; @@ -51,47 +57,6 @@ type SessionActionContext = { rememberSessionKey?: (sessionKey: string) => void | Promise; }; -type SessionInfoDefaults = { - model?: string | null; - modelProvider?: string | null; - contextTokens?: number | null; - thinkingLevels?: Array<{ id: string; label: string }>; -}; - -type SessionInfoEntry = SessionInfo & { - key?: string; - sessionId?: string; - modelOverride?: string; - providerOverride?: string; -}; - -function sessionInfoUiEquals(left: SessionInfo, right: SessionInfo): boolean { - return ( - left.thinkingLevel === right.thinkingLevel && - (left.thinkingLevels === right.thinkingLevels || - JSON.stringify(left.thinkingLevels ?? null) === - JSON.stringify(right.thinkingLevels ?? null)) && - left.fastMode === right.fastMode && - left.verboseLevel === right.verboseLevel && - left.traceLevel === right.traceLevel && - left.reasoningLevel === right.reasoningLevel && - left.model === right.model && - left.modelProvider === right.modelProvider && - left.agentRuntime?.id === right.agentRuntime?.id && - left.agentRuntime?.source === right.agentRuntime?.source && - left.agentRuntime?.fallback === right.agentRuntime?.fallback && - left.contextTokens === right.contextTokens && - left.inputTokens === right.inputTokens && - left.outputTokens === right.outputTokens && - left.totalTokens === right.totalTokens && - left.responseUsage === right.responseUsage && - left.effectiveResponseUsage === right.effectiveResponseUsage && - left.displayName === right.displayName && - (left.goal === right.goal || - JSON.stringify(left.goal ?? null) === JSON.stringify(right.goal ?? null)) - ); -} - export function createSessionActions(context: SessionActionContext) { const { client, @@ -661,6 +626,7 @@ export function createSessionActions(context: SessionActionContext) { setActivityStatus("idle"); if (selectionChanged) { state.currentSessionId = null; + clearTuiSessionModeOverrides(state.sessionInfo); } // Session keys can move backwards in updatedAt ordering; drop previous session freshness // so refresh data for the newly selected session isn't rejected as stale. diff --git a/src/tui/tui-session-info.ts b/src/tui/tui-session-info.ts new file mode 100644 index 000000000000..0544c3d7af99 --- /dev/null +++ b/src/tui/tui-session-info.ts @@ -0,0 +1,51 @@ +import type { SessionInfo } from "./tui-types.js"; + +export type SessionInfoDefaults = { + model?: string | null; + modelProvider?: string | null; + contextTokens?: number | null; + thinkingLevels?: Array<{ id: string; label: string }>; +}; + +export type SessionInfoEntry = SessionInfo & { + key?: string; + sessionId?: string; + modelOverride?: string; + providerOverride?: string; +}; + +/** Compare only session facts that change visible TUI behavior. */ +export function sessionInfoUiEquals(left: SessionInfo, right: SessionInfo): boolean { + return ( + left.thinkingLevel === right.thinkingLevel && + (left.thinkingLevels === right.thinkingLevels || + JSON.stringify(left.thinkingLevels ?? null) === + JSON.stringify(right.thinkingLevels ?? null)) && + left.fastMode === right.fastMode && + left.verboseLevel === right.verboseLevel && + left.traceLevel === right.traceLevel && + left.reasoningLevel === right.reasoningLevel && + left.model === right.model && + left.modelProvider === right.modelProvider && + left.agentRuntime?.id === right.agentRuntime?.id && + left.agentRuntime?.source === right.agentRuntime?.source && + left.agentRuntime?.fallback === right.agentRuntime?.fallback && + left.contextTokens === right.contextTokens && + left.inputTokens === right.inputTokens && + left.outputTokens === right.outputTokens && + left.totalTokens === right.totalTokens && + left.responseUsage === right.responseUsage && + left.effectiveResponseUsage === right.effectiveResponseUsage && + left.displayName === right.displayName && + (left.goal === right.goal || + JSON.stringify(left.goal ?? null) === JSON.stringify(right.goal ?? null)) + ); +} + +/** Clear selection-owned modes so a switch cannot display its predecessor while loading. */ +export function clearTuiSessionModeOverrides(sessionInfo: SessionInfo): void { + sessionInfo.fastMode = undefined; + sessionInfo.verboseLevel = undefined; + sessionInfo.traceLevel = undefined; + sessionInfo.reasoningLevel = undefined; +} diff --git a/src/tui/tui.ts b/src/tui/tui.ts index 96fc17a2f940..a7c90e18b3bf 100644 --- a/src/tui/tui.ts +++ b/src/tui/tui.ts @@ -44,12 +44,7 @@ import { editorTheme, theme } from "./theme/theme.js"; import type { TuiBackend } from "./tui-backend.js"; import { createCommandHandlers } from "./tui-command-handlers.js"; import { createEventHandlers } from "./tui-event-handlers.js"; -import { - formatGoalFooter, - formatModelFooter, - formatTuiErrorMessage, - formatTokens, -} from "./tui-formatters.js"; +import { formatTuiFooter, formatTuiErrorMessage } from "./tui-formatters.js"; import { buildTuiLastSessionScopeKey, readTuiLastSessionKey, @@ -1229,35 +1224,18 @@ export async function runTui(opts: RunTuiOptions): Promise { ? `${sessionKeyLabel} (${state.sessionInfo.displayName})` : sessionKeyLabel; const agentLabel = formatAgentLabel(state.currentAgentId); - const modelLabel = formatModelFooter({ - model: state.sessionInfo.model, - thinkingLevel: thinkingLevelOverride ?? state.sessionInfo.thinkingLevel, - }); - const tokens = formatTokens( - state.sessionInfo.totalTokens ?? null, - state.sessionInfo.contextTokens ?? null, + footer.setText( + theme.dim( + formatTuiFooter({ + agentLabel, + sessionLabel, + sessionInfo: state.sessionInfo, + thinkingLevel: thinkingLevelOverride ?? state.sessionInfo.thinkingLevel, + // Delivery is fixed at launch; session switches and patches cannot change it. + deliver: deliverDefault, + }), + ), ); - const fastLabel = - state.sessionInfo.fastMode === "auto" - ? "fast:auto" - : state.sessionInfo.fastMode === true - ? "fast" - : null; - const verbose = state.sessionInfo.verboseLevel ?? "off"; - const reasoning = state.sessionInfo.reasoningLevel ?? "off"; - const reasoningLabel = - reasoning === "on" ? "reasoning" : reasoning === "stream" ? "reasoning:stream" : null; - const footerParts = [ - `agent ${agentLabel}`, - `session ${sessionLabel}`, - modelLabel, - formatGoalFooter(state.sessionInfo.goal), - fastLabel, - verbose !== "off" ? `verbose ${verbose}` : null, - reasoningLabel, - tokens, - ].filter(Boolean); - footer.setText(theme.dim(footerParts.join(" | "))); }; const { openOverlay, closeOverlay } = createOverlayHandlers(tui, editor);