fix(tui): stabilize Windows terminal snapshots

Treat an absolute path.relative() result as a cross-volume path instead of incorrectly abbreviating it beneath the user's home directory.

Allow embeddings to project a logical footer cwd without changing the operational session cwd. The recorded-session harness now uses a POSIX-shaped display alias for both the footer and filesystem result paths, preserving the existing pre-normalization layout width on every host.

Keep runtime-provided labels behind terminal-control escaping, cover that boundary, and document the embedding contract.
This commit is contained in:
Tianyi Cui
2026-07-19 13:20:59 +08:00
parent bdba206640
commit 0f8d0082e4
5 changed files with 68 additions and 12 deletions
+22 -5
View File
@@ -6,7 +6,7 @@
*/
import { homedir } from 'node:os'
import { relative, resolve, sep } from 'node:path'
import { isAbsolute, relative, resolve, sep } from 'node:path'
import {
CombinedAutocompleteProvider,
Container,
@@ -135,6 +135,12 @@ export interface TuiRuntime {
terminal: Terminal
/** Exit hook used by terminal shutdown or a target-agent startup failure. */
exit(code: number): void
/**
* Override the footer's logical working-directory label without changing the session directory used by tools.
* @param cwd - Operational working directory from the session header.
* @returns Unescaped label; the TUI makes terminal controls visible.
*/
formatCwd?: (cwd: string | undefined) => string
}
/**
@@ -608,8 +614,10 @@ function formatCwd(cwd: string | undefined): string {
const home = homedir()
const rel = relative(resolve(home), resolve(cwd))
if (rel === '') return '~'
if (rel !== '..' && !rel.startsWith(`..${sep}`)) return displayText(`~${sep}${rel}`)
return displayText(cwd)
/* v8 ignore next -- Windows cross-drive coverage; POSIX relative() cannot return an absolute path. */
if (isAbsolute(rel)) return cwd
if (rel !== '..' && !rel.startsWith(`..${sep}`)) return `~${sep}${rel}`
return cwd
}
function sessionTokens(session: Session): { input: number; output: number } {
@@ -630,13 +638,15 @@ class FooterComponent implements Component {
private readonly toolsExpanded: () => boolean,
private readonly showReasoning: () => boolean,
private readonly tokens: () => { input: number; output: number },
private readonly cwdFormatter: TuiRuntime['formatCwd'],
) {}
invalidate(): void {}
render(width: number): string[] {
const { input, output } = this.tokens()
const left = `${formatCwd(this.agent.session.header.cwd)} ↑${formatTokens(input)} ↓${formatTokens(output)}`
const cwd = this.cwdFormatter?.(this.agent.session.header.cwd) ?? formatCwd(this.agent.session.header.cwd)
const left = `${displayText(cwd)} ↑${formatTokens(input)} ↓${formatTokens(output)}`
const right = `${this.agent.status} reasoning:${this.showReasoning() ? 'on' : 'off'} tools:${this.toolsExpanded() ? 'expanded' : 'compact'}`
const leftStyled = this.palette.dim(left)
const available = Math.max(0, width - visibleWidth(left) - 2)
@@ -843,7 +853,14 @@ export function createTuiChat(
const welcome = config.welcome ?? 'ready.'
const header = new HeaderComponent(agent, welcome, palette)
const footer = new FooterComponent(agent, palette, () => toolsExpanded, () => showReasoning, () => tokens)
const footer = new FooterComponent(
agent,
palette,
() => toolsExpanded,
() => showReasoning,
() => tokens,
runtime.formatCwd,
)
ui.addChild(header)
ui.addChild(chat)
ui.addChild(statusContainer)