feat(tui): add /status session diagnostics

This commit is contained in:
ZiyaZhang
2026-07-22 03:10:14 -07:00
parent 988dd70e7c
commit 280207c824
13 changed files with 316 additions and 42 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write # pnpm run verify-translation-pairing --write
2026-07-17-dedicated-full-screen-tui-front-door.md: 3e2b1e751001020eccc9193438daff23dd42518a 2026-07-17-dedicated-full-screen-tui-front-door.md: ecfda138593fc2b98ac42929acc586b11e437ee2
2026-07-17-dedicated-full-screen-tui-front-door.zh.md: 5f3632f43702e16ca9dec07c0bb8e42bf50499b7 2026-07-17-dedicated-full-screen-tui-front-door.zh.md: 6b8cc63f7657672a6da542e2033d765b54bd4f07

View File

@@ -22,7 +22,7 @@ The selected front door receives the exact generated or resumed `SessionId` used
The TUI rebuilds the transcript from the active `session.surface` and reprojects it whenever an event carries a `surfaceOp`, so resumed and compacted history matches the model-visible conversation. It renders Markdown text and reasoning, token totals, the latest `todo/write` plan, and tool cards produced through each tool definition's `presentCall` and `presentResult` methods. Long card bodies retain a configurable head/tail preview with the hidden-line count; one terminal control expands or collapses every card. Pending chunks and tool calls update the same components that completed events settle. The TUI rebuilds the transcript from the active `session.surface` and reprojects it whenever an event carries a `surfaceOp`, so resumed and compacted history matches the model-visible conversation. It renders Markdown text and reasoning, token totals, the latest `todo/write` plan, and tool cards produced through each tool definition's `presentCall` and `presentResult` methods. Long card bodies retain a configurable head/tail preview with the hidden-line count; one terminal control expands or collapses every card. Pending chunks and tool calls update the same components that completed events settle.
Editor input calls `agent.send()` while idle and `agent.steer()` while a turn is running. Cancellation, reasoning visibility, tool-card expansion, redraw, transcript clearing, and exit are terminal-only controls. The idle footer derives context occupancy from `tokenMeter` and pairs the selected model with its reasoning state; during a run, elapsed activity and the Escape interrupt hint replace that summary. The plugin registers the shared `userInteraction` provider and presents queued questions in a wide bottom-left keyboard panel with batch progress, numbered options, and aligned descriptions; agent behavior and answer logging remain owned by their existing services. Editor input calls `agent.send()` while idle and `agent.steer()` while a turn is running. Cancellation, reasoning visibility, tool-card expansion, redraw, transcript clearing, and exit are terminal-only controls. The idle footer derives context occupancy from `tokenMeter` and shows the selected model; during a run, elapsed activity and the Escape interrupt hint replace that summary. `/status` remains available in either state and appends a detailed terminal-only snapshot: session identity and timestamps, selected model and reasoning visibility, lifecycle counts folded from the event log, the same deduplicated usage buckets and KV-cache rate as the footer, and context use from `tokenMeter` plus the selected model's advertised capacity. The plugin registers the shared `userInteraction` provider and presents queued questions in a wide bottom-left keyboard panel with batch progress, numbered options, and aligned descriptions; agent behavior and answer logging remain owned by their existing services.
The `/model` command presents the advisory `ctx.llm` catalog as a keyboard selector and changes only this TUI session's target; argument forms remain available for direct selection. Agent-scoped prompt-assembly and request waterfalls snapshot one provider/model pair per step, so `{{provider}}` / `{{model}}` interpolation and request routing cannot split when a command arrives during assembly. The latest logged request header restores a used target; a selection that never reaches a request remains process-local. The `/model` command presents the advisory `ctx.llm` catalog as a keyboard selector and changes only this TUI session's target; argument forms remain available for direct selection. Agent-scoped prompt-assembly and request waterfalls snapshot one provider/model pair per step, so `{{provider}}` / `{{model}}` interpolation and request routing cannot split when a command arrives during assembly. The latest logged request header restores a used target; a selection that never reaches a request remains process-local.

View File

@@ -22,7 +22,7 @@ DeepSeek Harness 将 [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README
TUI 从活跃的 `session.surface` 重建 transcript文本记录并在事件携带 `surfaceOp` 时重新投影因此恢复或压缩后的历史与模型可见会话保持一致。TUI 渲染 Markdown 文本与推理、token 用量、最新 `todo/write` 计划,以及各工具定义通过 `presentCall``presentResult` 方法生成的工具卡片。较长的工具卡片正文会保留可配置的头尾预览,并显示隐藏行数;一个终端控制可以展开或收起全部卡片。进行中的分片与工具调用会更新同一组组件,随后由完成事件收束状态。 TUI 从活跃的 `session.surface` 重建 transcript文本记录并在事件携带 `surfaceOp` 时重新投影因此恢复或压缩后的历史与模型可见会话保持一致。TUI 渲染 Markdown 文本与推理、token 用量、最新 `todo/write` 计划,以及各工具定义通过 `presentCall``presentResult` 方法生成的工具卡片。较长的工具卡片正文会保留可配置的头尾预览,并显示隐藏行数;一个终端控制可以展开或收起全部卡片。进行中的分片与工具调用会更新同一组组件,随后由完成事件收束状态。
agent 空闲时,编辑器输入调用 `agent.send()`;轮次运行中则调用 `agent.steer()`。取消、推理显隐、工具卡片展开、重绘、清空 transcript 和退出都只是终端控制。空闲态页脚根据 `tokenMeter` 得出上下文占用率,并将选中模型及其推理状态组合显示agent 运行期间,该摘要会替换为带已用时长的活动指示和 Escape 中断提示。插件注册共享的 `userInteraction` 提供方在左下角宽幅键盘操作面板中呈现排队的问题面板显示批次进度、带编号的选项和对齐的描述agent 行为和答案日志仍由既有服务负责。 agent 空闲时,编辑器输入调用 `agent.send()`;轮次运行中则调用 `agent.steer()`。取消、推理显隐、工具卡片展开、重绘、清空 transcript 和退出都只是终端控制。空闲态页脚根据 `tokenMeter` 得出上下文占用率,并显示所选模型agent 运行期间,该摘要会替换为带已用时长的活动指示和 Escape 中断提示。`/status` 在这两种状态下均可用,并会追加一份仅在终端显示的详细快照,其中包括会话标识与时间戳、所选模型及推理显隐状态、从事件日志归并得出的生命周期计数、与页脚一致的去重用量分项和 KV 缓存命中率,以及 `tokenMeter` 给出的上下文用量和所选模型公布的容量。插件注册共享的 `userInteraction` 提供方在左下角宽幅键盘操作面板中呈现排队的问题面板显示批次进度、带编号的选项和对齐的描述agent 行为和答案日志仍由既有服务负责。
`/model` 命令将建议性的 `ctx.llm` 目录呈现为键盘选择器,并且只更改当前 TUI 会话的目标带参数的形式仍可直接选择目标。agent 作用域内的 prompt 组装和请求两条 waterfall瀑布式事件会为每个 step 快照一次同一个提供方/模型字段组合,因此即使命令在组装期间到达,`{{provider}}` / `{{model}}` 插值与请求路由也不会分裂。系统通过日志中最新的请求头恢复已经使用过的目标;未被请求使用的选择只保留在当前进程中。 `/model` 命令将建议性的 `ctx.llm` 目录呈现为键盘选择器,并且只更改当前 TUI 会话的目标带参数的形式仍可直接选择目标。agent 作用域内的 prompt 组装和请求两条 waterfall瀑布式事件会为每个 step 快照一次同一个提供方/模型字段组合,因此即使命令在组装期间到达,`{{provider}}` / `{{model}}` 插值与请求路由也不会分裂。系统通过日志中最新的请求头恢复已经使用过的目标;未被请求使用的选择只保留在当前进程中。

View File

@@ -17,7 +17,7 @@ Type a coding task. The agent works through the `read`/`write`/`edit` filesystem
The `todo_write` task tracker is opt-in and not in the shipped config: add `@deepseek-ai/dsh-tool-todo` to `cordis.yml` (or a personal-config overlay under `~/.dsh`) to expose it. Once loaded, the model records a whole-list plan to the session log and the TUI renders it. The `todo_write` task tracker is opt-in and not in the shipped config: add `@deepseek-ai/dsh-tool-todo` to `cordis.yml` (or a personal-config overlay under `~/.dsh`) to expose it. Once loaded, the model records a whole-list plan to the session log and the TUI renders it.
The TUI renders Markdown history, reasoning, tool-owned terminal/diff/generic cards, token totals, and — when `todo_write` is loaded — the latest plan. Long tool bodies keep a head/tail preview; Ctrl+O expands or collapses every card. Enter submits or steers while the agent runs, Ctrl+R toggles reasoning, Escape cancels, and `/help` lists commands. `/model` opens a keyboard selector for the current provider catalog; use Up/Down and Enter, or `/model <model>` and `/model <provider>/<model>` for direct selection. `ask_user_question` opens a wide bottom-left keyboard panel with batch progress and numbered options. The TUI renders Markdown history, reasoning, tool-owned terminal/diff/generic cards, token totals, and — when `todo_write` is loaded — the latest plan. Long tool bodies keep a head/tail preview; Ctrl+O expands or collapses every card. Enter submits or steers while the agent runs, Ctrl+R toggles reasoning, Escape cancels, and `/help` lists commands. `/status` expands the current session's identity, activity counts, exact token/cache buckets, context use, and timestamps without interrupting a running turn. `/model` opens a keyboard selector for the current provider catalog; use Up/Down and Enter, or `/model <model>` and `/model <provider>/<model>` for direct selection. `ask_user_question` opens a wide bottom-left keyboard panel with batch progress and numbered options.
### Resuming a prior session ### Resuming a prior session

View File

@@ -101,8 +101,10 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
// provider's tool-less title call; the scripted adapter answers it, the // provider's tool-less title call; the scripted adapter answers it, the
// accepted title lands in the log, and the TUI renders the terminal // accepted title lands in the log, and the TUI renders the terminal
// window title as `<session title> — <configured title>` via OSC 0. // window title as `<session title> — <configured title>` via OSC 0.
// Gating /exit on it keeps the assertion race-free. // Gating /status on it keeps the assertion race-free; the diagnostics
{ waitFor: 'scripted session title — DeepSeek Harness', send: '/exit\r' }, // card is then exercised through the same real Loader/PTY composition.
{ waitFor: 'scripted session title — DeepSeek Harness', send: '/status\r' },
{ waitFor: 'Session diagnostics', send: '/exit\r' },
], ],
}) })
expect(output).toContain('I need one decision before I continue.') expect(output).toContain('I need one decision before I continue.')
@@ -114,6 +116,14 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
expect(output).not.toContain('\u009B31mMODEL_C1') expect(output).not.toContain('\u009B31mMODEL_C1')
expect(output).toContain('Safe') expect(output).toContain('Safe')
expect(output).toContain('\u001B]0;scripted session title — DeepSeek Harness\u0007') expect(output).toContain('\u001B]0;scripted session title — DeepSeek Harness\u0007')
expect(output).toContain('Session diagnostics')
expect(output).toContain('Title')
expect(output).toContain('scripted session title')
expect(output).toContain('Model')
expect(output).toContain('tui-scripted/tui-scripted-model-pro')
expect(output).toContain('KV cache hit')
expect(output).toContain('Context')
expect(output).toContain('128,000')
expect(output).toContain('\u001B[?2004l') expect(output).toContain('\u001B[?2004l')
}, LOADER_SMOKE_TEST_TIMEOUT_MS) }, LOADER_SMOKE_TEST_TIMEOUT_MS)

View File

@@ -14,7 +14,7 @@ An embedding may provide `TuiRuntime.formatCwd` when its logical workspace label
Before model output, session events, tool presenters, questions, configuration, or diagnostics reach pi-tui's ANSI-aware renderers or the terminal title, the TUI renders C0 and C1 controls other than line feeds as visible `\xNN` text. Those sources cannot add terminal control sequences; the TUI and pi-tui retain ownership of terminal rendering and styling. Before model output, session events, tool presenters, questions, configuration, or diagnostics reach pi-tui's ANSI-aware renderers or the terminal title, the TUI renders C0 and C1 controls other than line feeds as visible `\xNN` text. Those sources cannot add terminal control sequences; the TUI and pi-tui retain ownership of terminal rendering and styling.
While the agent is running, ordinary editor submissions call `agent.steer()`; otherwise they call `agent.send()`. A slash at the start of the submitted line enters `ctx.commands` instead: known commands execute directly, unknown commands produce a warning, and neither path reaches the model. The TUI registers `/help`, `/model`, `/clear`, `/reasoning`, `/tools`, `/redraw`, `/reload`, `/resume`, and `/exit` as agent-scoped definitions; every other effective command joins autocomplete and `/help` dynamically, as do `/skill:` completions. A status line above the editor reports the turn phase the TUI derives from session events — waiting for the first token, thinking, responding, or executing tools — with the elapsed time in that phase and the running step total, refreshed each second, and ends with the `Enter sends steering, Esc cancels` hint; while steering messages wait to reach the model it inserts a `N queued ·` badge before the hint that clears as each drains. Ctrl+C or Escape cancels a running turn. Tool cards collapse long bodies into a configurable head/tail preview; Ctrl+O toggles every card between its preview and full output. Ctrl+R toggles reasoning, Ctrl+L redraws, and Ctrl+D exits while idle. While the agent is running, ordinary editor submissions call `agent.steer()`; otherwise they call `agent.send()`. A slash at the start of the submitted line enters `ctx.commands` instead: known commands execute directly, unknown commands produce a warning, and neither path reaches the model. The TUI registers `/help`, `/model`, `/clear`, `/reasoning`, `/tools`, `/redraw`, `/reload`, `/resume`, `/status`, and `/exit` as agent-scoped definitions; every other effective command joins autocomplete and `/help` dynamically, as do `/skill:` completions. A status line above the editor reports the turn phase the TUI derives from session events — waiting for the first token, thinking, responding, or executing tools — with the elapsed time in that phase and the running step total, refreshed each second, and ends with the `Enter sends steering, Esc cancels` hint; while steering messages wait to reach the model it inserts a `N queued ·` badge before the hint that clears as each drains. Ctrl+C or Escape cancels a running turn. Tool cards collapse long bodies into a configurable head/tail preview; Ctrl+O toggles every card between its preview and full output. Ctrl+R toggles reasoning, Ctrl+L redraws, and Ctrl+D exits while idle.
`/model` opens the advisory `ctx.llm` catalog as a keyboard selector: Up/Down moves, Enter selects, and Escape closes it. `/model <model>` still selects an unambiguous model id directly, while `/model <provider>/<model>` selects an exact target. The configured target or latest logged request header initializes the selector, and an unlisted current model remains visible because catalogs are advisory. Selection is local to this TUI session. Prompt assembly snapshots the target for one step, replaces `{{provider}}` and `{{model}}`, and applies the same pair through `agent/request`; a switch during assembly therefore starts with a later step. The request header durably records targets that reach the model, while an unused selection remains process-local. `/model` opens the advisory `ctx.llm` catalog as a keyboard selector: Up/Down moves, Enter selects, and Escape closes it. `/model <model>` still selects an unambiguous model id directly, while `/model <provider>/<model>` selects an exact target. The configured target or latest logged request header initializes the selector, and an unlisted current model remains visible because catalogs are advisory. Selection is local to this TUI session. Prompt assembly snapshots the target for one step, replaces `{{provider}}` and `{{model}}`, and applies the same pair through `agent/request`; a switch during assembly therefore starts with a later step. The request header durably records targets that reach the model, while an unused selection remains process-local.
@@ -22,6 +22,8 @@ While the agent is running, ordinary editor submissions call `agent.steer()`; ot
The footer sums the session's reported usage as `↑<uncached input> ↓<output>`, followed by `cache <rate>%` once any input has been billed — the share of billed prompt tokens (uncached input plus cache reads and writes) served from the provider cache, rounded to a percent. It also compares token-meter pressure with `ctx.llm.resolveModelContext()` for the current route (omitting the context share when the adapter has no capacity metadata) and shows the current model and tool-card mode; the right side clips first when the footer is narrow. The footer sums the session's reported usage as `↑<uncached input> ↓<output>`, followed by `cache <rate>%` once any input has been billed — the share of billed prompt tokens (uncached input plus cache reads and writes) served from the provider cache, rounded to a percent. It also compares token-meter pressure with `ctx.llm.resolveModelContext()` for the current route (omitting the context share when the adapter has no capacity metadata) and shows the current model and tool-card mode; the right side clips first when the footer is narrow.
`/status` adds a point-in-time diagnostics card to the transcript and remains available while the agent runs. It reports the session id, title, working directory, selected provider/model, reasoning-block visibility, agent state, event/turn/step/tool-call counts, exact input/output/cache token buckets, KV-cache hit rate, token-meter context use and capacity, creation time, and latest event time. Missing titles, models, cache input, or context capacity are labeled instead of inferred. The card is terminal-only and does not duplicate the compact footer.
When `resumeCommand` is set and a `sessionPersistence` backend is mounted, exiting prints the resume command for the current session (once it has been persisted, so an abandoned session yields no hint), and `/resume` lists this workspace's persisted sessions newest-first, each with its resume command and a marker on the current one. `{session}` in the template expands to the session id; the TUI only prints commands to copy and never resumes in place. When `resumeCommand` is set and a `sessionPersistence` backend is mounted, exiting prints the resume command for the current session (once it has been persisted, so an abandoned session yields no hint), and `/resume` lists this workspace's persisted sessions newest-first, each with its resume command and a marker on the current one. `{session}` in the template expands to the session id; the TUI only prints commands to copy and never resumes in place.
## Config ## Config

View File

@@ -940,6 +940,14 @@ function sessionTokens(session: Session): SessionTokenTotals {
return totals return totals
} }
function formatDiagnosticNumber(value: number): string {
return value.toLocaleString('en-US')
}
function formatDiagnosticTime(value: number): string {
return new Date(value).toISOString()
}
class FooterComponent implements Component { class FooterComponent implements Component {
constructor( constructor(
private readonly agent: Agent, private readonly agent: Agent,
@@ -1953,6 +1961,44 @@ export function createTuiChat(
requestRender() requestRender()
} }
const showStatus = (): void => {
const events = agent.session.events
const latestActivity = events.at(-1)?.time ?? agent.session.header.createdAt
const usedContext = Math.max(0, Math.round(ctx.tokenMeter.measure(agent.session).totalTokens))
const context = contextWindow === undefined
? `${formatDiagnosticNumber(usedContext)} used · capacity unknown`
: `${formatDiagnosticNumber(usedContext)} / ${formatDiagnosticNumber(contextWindow)} (${String(Math.round(usedContext / contextWindow * 100))}%)`
const rate = cacheHitRate(tokens)
const rows = [
['Session', agent.session.id],
['Title', sessionTitle ?? 'untitled'],
['Working dir', cwd],
['Model', target.current === undefined ? 'unset' : targetLabel(target.current)],
['Reasoning view', showReasoning ? 'shown' : 'hidden'],
['Agent', agent.status],
['Activity', [
`events ${String(events.length)}`,
`turns ${String(events.filter(event => event.type === 'turn/start').length)}`,
`steps ${String(events.filter(event => event.type === 'step/start').length)}`,
`tool calls ${String(events.filter(event => event.type === 'tool/call').length)}`,
].join(' · ')],
['Tokens', `input ${formatDiagnosticNumber(tokens.input)} · output ${formatDiagnosticNumber(tokens.output)}`],
['Cache tokens', `read ${formatDiagnosticNumber(tokens.cacheRead)} · write ${formatDiagnosticNumber(tokens.cacheWrite)}`],
['KV cache hit', rate === undefined ? 'n/a' : `${String(rate)}%`],
['Context', context],
['Created', formatDiagnosticTime(agent.session.header.createdAt)],
['Last active', formatDiagnosticTime(latestActivity)],
] as const
const labelWidth = Math.max(...rows.map(([label]) => label.length))
const card = new GutterBox(text => palette.accent(text), 0)
card.addChild(new Text(palette.bold(palette.accent('Session diagnostics')), 0, 0))
card.addChild(new Text(rows.map(([label, value]) =>
`${palette.muted(label.padEnd(labelWidth))} ${displayText(value)}`).join('\n'), 0, 0))
chat.addChild(new Spacer(1))
chat.addChild(card)
requestRender()
}
// Skill listing is async while `createTuiChat` is synchronous, so the // Skill listing is async while `createTuiChat` is synchronous, so the
// completions rebuild once the catalog resolves. Disabled-for-model skills // completions rebuild once the catalog resolves. Disabled-for-model skills
// are absent from `list()`, so they never appear as completions; a user can // are absent from `list()`, so they never appear as completions; a user can
@@ -2041,6 +2087,11 @@ export function createTuiChat(
description: 'List this workspace\'s resumable sessions', description: 'List this workspace\'s resumable sessions',
handler: () => { showResume(); return { kind: 'success' } }, handler: () => { showResume(); return { kind: 'success' } },
}) })
commandCtx.commands.register({
name: 'status',
description: 'Show detailed session diagnostics',
handler: () => { showStatus(); return { kind: 'success' } },
})
commandCtx.commands.register({ commandCtx.commands.register({
name: 'exit', name: 'exit',
description: 'Exit after the active turn reaches idle', description: 'Exit after the active turn reaches idle',

View File

@@ -24,6 +24,8 @@ interface FakeAgent extends Agent {
export interface TuiHarnessOptions { export interface TuiHarnessOptions {
status?: AgentStatus status?: AgentStatus
config?: Config config?: Config
/** Leave the session event log empty instead of seeding one turn and step. */
omitInitialLifecycle?: boolean
/** Omit the harness's default `welcome`, exercising the banner sweep-reveal path. */ /** Omit the harness's default `welcome`, exercising the banner sweep-reveal path. */
omitWelcome?: boolean omitWelcome?: boolean
tools?: Record<string, ToolDefinition> tools?: Record<string, ToolDefinition>
@@ -120,11 +122,13 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
sessionId, sessionId,
options.cwd === null ? undefined : { meta: { cwd: options.cwd ?? '/workspace' } }, options.cwd === null ? undefined : { meta: { cwd: options.cwd ?? '/workspace' } },
) )
session.append('turn/start', { if (options.omitInitialLifecycle !== true) {
turn: 1, session.append('turn/start', {
trigger: { kind: 'message', source: { kind: 'user' } }, turn: 1,
}) trigger: { kind: 'message', source: { kind: 'user' } },
session.append('step/start', { turn: 1, step: 1 }) })
session.append('step/start', { turn: 1, step: 1 })
}
options.beforeMount?.(session) options.beforeMount?.(session)
const sent: ContentBlock[][] = [] const sent: ContentBlock[][] = []
const steered: ContentBlock[][] = [] const steered: ContentBlock[][] = []

View File

@@ -1,7 +1,7 @@
terminal 92x32 buffer=normal length=32 base=0 viewport=0 terminal 92x32 buffer=normal length=32 base=0 viewport=0
lifecycle started=1 stopped=1 progress=inactive lifecycle started=1 stopped=1 progress=inactive
title "DSH snapshot" title "DSH snapshot"
cursor visible column=0 viewportRow=30 bufferRow=30 cursor visible column=0 viewportRow=31 bufferRow=31
buffer buffer
0| " DEEPSEEK HARNESS" 0| " DEEPSEEK HARNESS"
style 1-8 fg=bright-blue bold style 1-8 fg=bright-blue bold
@@ -36,26 +36,28 @@ buffer
style 1-88 fg=bright-black style 1-88 fg=bright-black
16| " /resume — List this workspace's resumable sessions " 16| " /resume — List this workspace's resumable sessions "
style 1-50 fg=bright-black style 1-50 fg=bright-black
17| " /tools — Expand or collapse all tool cards " 17| " /status — Show detailed session diagnostics "
style 1-43 fg=bright-black
18| " /tools — Expand or collapse all tool cards "
style 1-42 fg=bright-black style 1-42 fg=bright-black
18| " /skill:<name> [instructions] — load a skill into the conversation " 19| " /skill:<name> [instructions] — load a skill into the conversation "
style 1-65 fg=bright-black style 1-65 fg=bright-black
19| <blank> 20| <blank>
20| " provider stream failed after partial output " 21| " provider stream failed after partial output "
style 1-43 fg=red style 1-43 fg=red
21| <blank> 22| <blank>
22| " The previous process ended during this turn. " 23| " The previous process ended during this turn. "
style 1-44 fg=yellow style 1-44 fg=yellow
23| <blank> 24| <blank>
24| " Unknown command: /unknown-advanced-command " 25| " Unknown command: /unknown-advanced-command "
style 1-42 fg=yellow style 1-42 fg=yellow
25| "────────────────────────────────────────────────────────────────────────────────────────────" 26| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim style 0-91 dim
26| " " 27| " "
style 1-1 inverse style 1-1 inverse
27| "────────────────────────────────────────────────────────────────────────────────────────────" 28| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim style 0-91 dim
28| "deepseek-v4-flash /workspace/project ↑0 ↓0 0% context tools:collapsed" 29| "deepseek-v4-flash /workspace/project ↑0 ↓0 0% context tools:collapsed"
style 0-43 dim style 0-43 dim
style 65-91 dim style 65-91 dim
29-31| <blank> 30-31| <blank>

View File

@@ -1,7 +1,7 @@
terminal 92x32 buffer=normal length=32 base=0 viewport=0 terminal 92x32 buffer=normal length=32 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot" title "DSH snapshot"
cursor hidden column=1 viewportRow=26 bufferRow=26 cursor hidden column=1 viewportRow=27 bufferRow=27
buffer buffer
0| " DEEPSEEK HARNESS" 0| " DEEPSEEK HARNESS"
style 1-8 fg=bright-blue bold style 1-8 fg=bright-blue bold
@@ -36,26 +36,28 @@ buffer
style 1-88 fg=bright-black style 1-88 fg=bright-black
16| " /resume — List this workspace's resumable sessions " 16| " /resume — List this workspace's resumable sessions "
style 1-50 fg=bright-black style 1-50 fg=bright-black
17| " /tools — Expand or collapse all tool cards " 17| " /status — Show detailed session diagnostics "
style 1-43 fg=bright-black
18| " /tools — Expand or collapse all tool cards "
style 1-42 fg=bright-black style 1-42 fg=bright-black
18| " /skill:<name> [instructions] — load a skill into the conversation " 19| " /skill:<name> [instructions] — load a skill into the conversation "
style 1-65 fg=bright-black style 1-65 fg=bright-black
19| <blank> 20| <blank>
20| " provider stream failed after partial output " 21| " provider stream failed after partial output "
style 1-43 fg=red style 1-43 fg=red
21| <blank> 22| <blank>
22| " The previous process ended during this turn. " 23| " The previous process ended during this turn. "
style 1-44 fg=yellow style 1-44 fg=yellow
23| <blank> 24| <blank>
24| " Unknown command: /unknown-advanced-command " 25| " Unknown command: /unknown-advanced-command "
style 1-42 fg=yellow style 1-42 fg=yellow
25| "────────────────────────────────────────────────────────────────────────────────────────────" 26| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim style 0-91 dim
26| " " 27| " "
style 1-1 inverse style 1-1 inverse
27| "────────────────────────────────────────────────────────────────────────────────────────────" 28| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim style 0-91 dim
28| "deepseek-v4-flash /workspace/project ↑0 ↓0 0% context tools:collapsed" 29| "deepseek-v4-flash /workspace/project ↑0 ↓0 0% context tools:collapsed"
style 0-43 dim style 0-43 dim
style 65-91 dim style 65-91 dim
29-31| <blank> 30-31| <blank>

View File

@@ -0,0 +1,79 @@
terminal 92x32 buffer=normal length=32 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "Inspect session diagnostics — DSH snapshot"
cursor hidden column=1 viewportRow=27 bufferRow=27
buffer
0| " DEEPSEEK HARNESS"
style 1-8 fg=bright-blue bold
style 10-16 bold
1| " Inspect session diagnostics"
style 1-27 fg=bright-black
2| " deepseek-v4-pro • main-session"
style 1-32 dim
3| <blank>
4| "▌ "
style 0-0 fg=bright-blue
5| "▌ You "
style 0-0 fg=bright-blue
style 2-4 fg=bright-blue bold
6| "▌ inspect this session "
style 0-0 fg=bright-blue
7| "▌ "
style 0-0 fg=bright-blue
8| <blank>
9| " Assistant "
style 1-9 fg=bright-magenta bold
10| " Session inspected. "
11| <blank>
12| "▌ Session diagnostics "
style 0-0 fg=bright-blue
style 2-20 fg=bright-blue bold
13| "▌ Session main-session "
style 0-0 fg=bright-blue
style 2-15 fg=bright-black
14| "▌ Title Inspect session diagnostics "
style 0-0 fg=bright-blue
style 2-15 fg=bright-black
15| "▌ Working dir /workspace/project "
style 0-0 fg=bright-blue
style 2-15 fg=bright-black
16| "▌ Model deepseek/deepseek-v4-pro "
style 0-0 fg=bright-blue
style 2-15 fg=bright-black
17| "▌ Reasoning view shown "
style 0-0 fg=bright-blue
style 2-15 fg=bright-black
18| "▌ Agent idle "
style 0-0 fg=bright-blue
style 2-15 fg=bright-black
19| "▌ Activity events 6 · turns 1 · steps 1 · tool calls 1 "
style 0-0 fg=bright-blue
style 2-15 fg=bright-black
20| "▌ Tokens input 1,250 · output 340 "
style 0-0 fg=bright-blue
style 2-15 fg=bright-black
21| "▌ Cache tokens read 3,000 · write 250 "
style 0-0 fg=bright-blue
style 2-15 fg=bright-black
22| "▌ KV cache hit 67% "
style 0-0 fg=bright-blue
style 2-15 fg=bright-black
23| "▌ Context 42,000 / 128,000 (33%) "
style 0-0 fg=bright-blue
style 2-15 fg=bright-black
24| "▌ Created 2026-07-22T09:10:11.000Z "
style 0-0 fg=bright-blue
style 2-15 fg=bright-black
25| "▌ Last active 2026-07-22T09:10:11.000Z "
style 0-0 fg=bright-blue
style 2-15 fg=bright-black
26| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim
27| " "
style 1-1 inverse
28| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim
29| "deepseek-v4-pro /workspace/project ↑1.3k ↓340 cache 67% 33% context tools:collapsed"
style 0-57 dim
style 64-91 dim
30-31| <blank>

View File

@@ -1,7 +1,7 @@
import { mkdir, readdir, writeFile } from 'node:fs/promises' import { mkdir, readdir, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path' import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { afterAll, describe, expect, it } from 'vitest' import { afterAll, describe, expect, it, vi } from 'vitest'
import type { Context } from 'cordis' import type { Context } from 'cordis'
import { agentEvents } from '@deepseek-ai/dsh-agent' import { agentEvents } from '@deepseek-ai/dsh-agent'
import { CallId, type ContentBlock } from '@deepseek-ai/dsh-llm' import { CallId, type ContentBlock } from '@deepseek-ai/dsh-llm'
@@ -48,6 +48,7 @@ const CHECKPOINTS = [
'errors-and-help', 'errors-and-help',
'disposed-terminal', 'disposed-terminal',
'resume-sessions', 'resume-sessions',
'status-diagnostics',
] as const ] as const
type Checkpoint = typeof CHECKPOINTS[number] type Checkpoint = typeof CHECKPOINTS[number]
@@ -637,6 +638,43 @@ describe('TUI terminal-state snapshots', () => {
await checkpoint('resume-sessions', harness.terminal, { includeScrollback: true }) await checkpoint('resume-sessions', harness.terminal, { includeScrollback: true })
await disposeSnapshot(harness) await disposeSnapshot(harness)
}) })
it('pins the detailed session diagnostics card', async () => {
const dateNow = vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-07-22T09:10:11.000Z'))
const harness = await setupSnapshot({
contextWindow: 128_000,
contextTokens: 42_000,
agentOptions: { provider: 'deepseek', model: 'deepseek-v4-pro' },
beforeMount(session) {
appendUser(session, 'inspect this session')
appendAssistant(session, [{ type: 'text', text: 'Session inspected.' }], {
inputTokens: 1_250,
outputTokens: 340,
cacheReadTokens: 3_000,
cacheWriteTokens: 250,
})
session.append('tool/call', {
turn: 1,
step: 1,
callId: CallId('status-call'),
name: 'read',
arguments: '{"path":"README.md"}',
})
session.append('session/title', {
title: 'Inspect session diagnostics',
messageSeqs: [1],
source: { kind: 'fallback' },
})
},
}, { columns: 92, rows: 32 })
await renderAfter(harness, () => {
harness.terminal.send('/status')
harness.terminal.send('\r')
})
await checkpoint('status-diagnostics', harness.terminal, { includeScrollback: true })
await disposeSnapshot(harness)
dateNow.mockRestore()
})
}) })
afterAll(async () => { afterAll(async () => {

View File

@@ -845,6 +845,92 @@ describe('pi-tui chat lifecycle and transcript', () => {
await dispose(result) await dispose(result)
}) })
it('shows detailed session diagnostics while the agent is running', async () => {
const timestamp = Date.parse('2026-07-22T09:10:11.000Z')
const dateNow = vi.spyOn(Date, 'now').mockReturnValue(timestamp)
const result = await setup({
cwd: '/workspace/status',
contextWindow: 128_000,
contextTokens: 42_000,
config: { showReasoning: false },
agentOptions: { provider: 'deepseek', model: 'deepseek-v4-pro' },
beforeMount(session) {
session.append('session/title', {
title: 'Inspect status \u001B]2;unsafe\u0007',
messageSeqs: [1],
source: { kind: 'fallback' },
})
appendAssistant(session, [{ type: 'text', text: 'measured' }], {
inputTokens: 1_250,
outputTokens: 340,
cacheReadTokens: 3_000,
cacheWriteTokens: 250,
})
session.append('tool/call', {
turn: 1, step: 1, callId: 'status-call-1' as never, name: 'read', arguments: '{}',
})
session.append('tool/call', {
turn: 1, step: 1, callId: 'status-call-2' as never, name: 'write', arguments: '{}',
})
},
})
result.agent.status = 'running'
agentEvents(result.ctx, result.agent).emit('agent/status', 'running')
result.terminal.send('/status')
result.terminal.send('\r')
await tick()
expect(result.terminal.output).toContain('Session diagnostics')
expect(result.terminal.output).toContain('Session main-session')
expect(result.terminal.output).toContain('Title Inspect status \\x1b]2;unsafe\\x07')
expect(result.terminal.output).toContain('Working dir /workspace/status')
expect(result.terminal.output).toContain('Model deepseek/deepseek-v4-pro')
expect(result.terminal.output).toContain('Reasoning view hidden')
expect(result.terminal.output).toContain('Agent running')
expect(result.terminal.output).toContain('Activity events 6 · turns 1 · steps 1 · tool calls 2')
expect(result.terminal.output).toContain('Tokens input 1,250 · output 340')
expect(result.terminal.output).toContain('Cache tokens read 3,000 · write 250')
expect(result.terminal.output).toContain('KV cache hit 67%')
expect(result.terminal.output).toContain('Context 42,000 / 128,000 (33%)')
expect(result.terminal.output).toContain('Created 2026-07-22T09:10:11.000Z')
expect(result.terminal.output).toContain('Last active 2026-07-22T09:10:11.000Z')
expect(result.terminal.output).not.toContain('\u001B]2;unsafe\u0007')
await dispose(result)
dateNow.mockRestore()
})
it('labels unavailable status diagnostics without inventing values', async () => {
const timestamp = Date.parse('2026-07-22T10:11:12.000Z')
const dateNow = vi.spyOn(Date, 'now').mockReturnValue(timestamp)
const result = await setup({
cwd: null,
omitInitialLifecycle: true,
contextTokens: 7,
agentOptions: {},
catalog: {
providers: [],
models: [],
resolveModelContext: () => Promise.resolve(undefined),
},
})
result.terminal.send('/status')
result.terminal.send('\r')
await tick()
expect(result.terminal.output).toContain('Title untitled')
expect(result.terminal.output).toContain('Model unset')
expect(result.terminal.output).toContain('Reasoning view shown')
expect(result.terminal.output).toContain('Agent idle')
expect(result.terminal.output).toContain('Activity events 0 · turns 0 · steps 0 · tool calls 0')
expect(result.terminal.output).toContain('KV cache hit n/a')
expect(result.terminal.output).toContain('Context 7 used · capacity unknown')
expect(result.terminal.output).toContain('Created 2026-07-22T10:11:12.000Z')
expect(result.terminal.output).toContain('Last active 2026-07-22T10:11:12.000Z')
await dispose(result)
dateNow.mockRestore()
})
it('sends, steers, handles commands, global keys, and disposed-agent input', async () => { it('sends, steers, handles commands, global keys, and disposed-agent input', async () => {
const result = await setup() const result = await setup()