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
@@ -2,5 +2,5 @@
# 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:
# 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.zh.md: 5f3632f43702e16ca9dec07c0bb8e42bf50499b7
2026-07-17-dedicated-full-screen-tui-front-door.md: ecfda138593fc2b98ac42929acc586b11e437ee2
2026-07-17-dedicated-full-screen-tui-front-door.zh.md: 6b8cc63f7657672a6da542e2033d765b54bd4f07
@@ -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.
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.
@@ -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` 方法生成的工具卡片。较长的工具卡片正文会保留可配置的头尾预览,并显示隐藏行数;一个终端控制可以展开或收起全部卡片。进行中的分片与工具调用会更新同一组组件,随后由完成事件收束状态。
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}}` 插值与请求路由也不会分裂。系统通过日志中最新的请求头恢复已经使用过的目标;未被请求使用的选择只保留在当前进程中。
+1 -1
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 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
@@ -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
// accepted title lands in the log, and the TUI renders the terminal
// window title as `<session title> — <configured title>` via OSC 0.
// Gating /exit on it keeps the assertion race-free.
{ waitFor: 'scripted session title — DeepSeek Harness', send: '/exit\r' },
// Gating /status on it keeps the assertion race-free; the diagnostics
// 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.')
@@ -114,6 +116,14 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
expect(output).not.toContain('\u009B31mMODEL_C1')
expect(output).toContain('Safe')
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')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
+3 -1
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.
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.
@@ -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.
`/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.
## Config
+51
View File
@@ -940,6 +940,14 @@ function sessionTokens(session: Session): SessionTokenTotals {
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 {
constructor(
private readonly agent: Agent,
@@ -1953,6 +1961,44 @@ export function createTuiChat(
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
// completions rebuild once the catalog resolves. Disabled-for-model skills
// 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',
handler: () => { showResume(); return { kind: 'success' } },
})
commandCtx.commands.register({
name: 'status',
description: 'Show detailed session diagnostics',
handler: () => { showStatus(); return { kind: 'success' } },
})
commandCtx.commands.register({
name: 'exit',
description: 'Exit after the active turn reaches idle',
+9 -5
View File
@@ -24,6 +24,8 @@ interface FakeAgent extends Agent {
export interface TuiHarnessOptions {
status?: AgentStatus
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. */
omitWelcome?: boolean
tools?: Record<string, ToolDefinition>
@@ -120,11 +122,13 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
sessionId,
options.cwd === null ? undefined : { meta: { cwd: options.cwd ?? '/workspace' } },
)
session.append('turn/start', {
turn: 1,
trigger: { kind: 'message', source: { kind: 'user' } },
})
session.append('step/start', { turn: 1, step: 1 })
if (options.omitInitialLifecycle !== true) {
session.append('turn/start', {
turn: 1,
trigger: { kind: 'message', source: { kind: 'user' } },
})
session.append('step/start', { turn: 1, step: 1 })
}
options.beforeMount?.(session)
const sent: ContentBlock[][] = []
const steered: ContentBlock[][] = []
@@ -1,7 +1,7 @@
terminal 92x32 buffer=normal length=32 base=0 viewport=0
lifecycle started=1 stopped=1 progress=inactive
title "DSH snapshot"
cursor visible column=0 viewportRow=30 bufferRow=30
cursor visible column=0 viewportRow=31 bufferRow=31
buffer
0| " DEEPSEEK HARNESS"
style 1-8 fg=bright-blue bold
@@ -36,26 +36,28 @@ buffer
style 1-88 fg=bright-black
16| " /resume — List this workspace's resumable sessions "
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
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
19| <blank>
20| " provider stream failed after partial output "
20| <blank>
21| " provider stream failed after partial output "
style 1-43 fg=red
21| <blank>
22| " The previous process ended during this turn. "
22| <blank>
23| " The previous process ended during this turn. "
style 1-44 fg=yellow
23| <blank>
24| " Unknown command: /unknown-advanced-command "
24| <blank>
25| " Unknown command: /unknown-advanced-command "
style 1-42 fg=yellow
25| "────────────────────────────────────────────────────────────────────────────────────────────"
26| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim
26| " "
27| " "
style 1-1 inverse
27| "────────────────────────────────────────────────────────────────────────────────────────────"
28| "────────────────────────────────────────────────────────────────────────────────────────────"
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 65-91 dim
29-31| <blank>
30-31| <blank>
@@ -1,7 +1,7 @@
terminal 92x32 buffer=normal length=32 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=26 bufferRow=26
cursor hidden column=1 viewportRow=27 bufferRow=27
buffer
0| " DEEPSEEK HARNESS"
style 1-8 fg=bright-blue bold
@@ -36,26 +36,28 @@ buffer
style 1-88 fg=bright-black
16| " /resume — List this workspace's resumable sessions "
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
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
19| <blank>
20| " provider stream failed after partial output "
20| <blank>
21| " provider stream failed after partial output "
style 1-43 fg=red
21| <blank>
22| " The previous process ended during this turn. "
22| <blank>
23| " The previous process ended during this turn. "
style 1-44 fg=yellow
23| <blank>
24| " Unknown command: /unknown-advanced-command "
24| <blank>
25| " Unknown command: /unknown-advanced-command "
style 1-42 fg=yellow
25| "────────────────────────────────────────────────────────────────────────────────────────────"
26| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim
26| " "
27| " "
style 1-1 inverse
27| "────────────────────────────────────────────────────────────────────────────────────────────"
28| "────────────────────────────────────────────────────────────────────────────────────────────"
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 65-91 dim
29-31| <blank>
30-31| <blank>
@@ -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>
+39 -1
View File
@@ -1,7 +1,7 @@
import { mkdir, readdir, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
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 { agentEvents } from '@deepseek-ai/dsh-agent'
import { CallId, type ContentBlock } from '@deepseek-ai/dsh-llm'
@@ -48,6 +48,7 @@ const CHECKPOINTS = [
'errors-and-help',
'disposed-terminal',
'resume-sessions',
'status-diagnostics',
] as const
type Checkpoint = typeof CHECKPOINTS[number]
@@ -637,6 +638,43 @@ describe('TUI terminal-state snapshots', () => {
await checkpoint('resume-sessions', harness.terminal, { includeScrollback: true })
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 () => {
+86
View File
@@ -845,6 +845,92 @@ describe('pi-tui chat lifecycle and transcript', () => {
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 () => {
const result = await setup()