feat(tui): bare /details opens a keyboard selector
DetailsDialog is a centered SelectList over the five transcript-detail states (three tool-card phases, reasoning shown/hidden); it preselects the current phase, marks both current values, applies on Enter, and cancels on Esc/Ctrl+C. Width is the new detailsDialogWidth config key. The argument grammar is unchanged and shares the same setters.
This commit is contained in:
@@ -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 .agents/notes/implemented/feature/2026-07-30-tui-details-command.md
|
||||
2026-07-30-tui-details-command.md: f17c168cb87d546ca158eddef7dd96fb0ab8be2d
|
||||
2026-07-30-tui-details-command.zh.md: 48e9a8d3f18c51982b8678e39d9315ffe08b015f
|
||||
2026-07-30-tui-details-command.md: c14461aa08790033026eead38f53d4b8e686bbbb
|
||||
2026-07-30-tui-details-command.zh.md: e9c9787a4911f29f49e7709d1ba673dc1a25e8a1
|
||||
@@ -10,7 +10,7 @@ The TUI's transcript detail state — tool-card visibility (`collapsed`/`expande
|
||||
|
||||
## Decision
|
||||
|
||||
`dsh-tui` registers `/details` beside its other agent-scoped commands. Bare `/details` reports the current state in one notice. Arguments name target states directly: `collapsed|expanded|hidden` jumps tool cards to that phase, `reasoning on|off` sets reasoning display, bare `reasoning` toggles it, and directives combine in one invocation. An unknown token returns a command error carrying the usage line. The command mutates the same closure state as the shortcuts, refactored so the cycle and toggle are thin wrappers over `setToolsVisibility`/`setReasoning`; the shortcuts and their notices are unchanged.
|
||||
`dsh-tui` registers `/details` beside its other agent-scoped commands. Bare `/details` opens `DetailsDialog`, a centered keyboard selector over the five detail states — the three tool-card phases and reasoning shown/hidden — that preselects the current phase, marks both current values, applies the highlighted state on Enter, and cancels on Esc or Ctrl+C; its width is the `detailsDialogWidth` config key and a second `/details` replaces an open selector, mirroring the `/model` overlay. Arguments name target states directly: `collapsed|expanded|hidden` jumps tool cards to that phase, `reasoning on|off` sets reasoning display, bare `reasoning` toggles it, and directives combine in one invocation. An unknown token returns a command error carrying the usage line. Every entry mutates the same closure state as the shortcuts, refactored so the cycle and toggle are thin wrappers over `setToolsVisibility`/`setReasoning`; the shortcuts and their notices are unchanged.
|
||||
|
||||
A combined invocation applies reasoning before visibility because `setReasoning` rebuilds the transcript from session events, which drops non-durable notice components; applying it last would erase the just-appended visibility notice.
|
||||
|
||||
@@ -18,7 +18,9 @@ The reasoning rebuild exposed a replay defect that this change fixes in `renderE
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Cycle on bare `/details`, mirroring Ctrl+O.** Rejected: the command's value over the shortcut is naming an absolute state; a cycling command is the shortcut with more keystrokes, and bare invocation is more useful as a state report.
|
||||
**Cycle on bare `/details`, mirroring Ctrl+O.** Rejected: the command's value over the shortcut is naming an absolute state; a cycling command is the shortcut with more keystrokes, and bare invocation is more useful as the selector, which shows the current state while offering every target.
|
||||
|
||||
**Bare `/details` as a text-only state report.** Shipped first, replaced by the selector: the report answered "where am I" but still required a second, argument-spelling invocation to change anything, while the selector shows the same state and applies a change in one interaction. The textual grammar remains for scripts, muscle memory, and combined two-dimension changes.
|
||||
|
||||
**Separate `/tools` and `/reasoning` commands.** Rejected: both dimensions are one presentation concern ("how much detail does the transcript show"), and a single command keeps the registry and `/help` list small while allowing one combined invocation.
|
||||
|
||||
@@ -26,6 +28,7 @@ The reasoning rebuild exposed a replay defect that this change fixes in `renderE
|
||||
|
||||
## Consequences
|
||||
|
||||
- A user can jump to any detail mode, set both dimensions at once, and query the state — including on terminals that intercept Ctrl+O/Ctrl+R.
|
||||
- A user can jump to any detail mode, set both dimensions at once, and see the current state in the selector — including on terminals that intercept Ctrl+O/Ctrl+R.
|
||||
- The parser accepts order-free tokens, so `/details reasoning expanded` toggles reasoning and expands cards; last directive wins per dimension. This leniency is deliberate and documented in the README.
|
||||
- Transcript rebuilds no longer lose assistant messages when a step carries more than one `assistant/message` event; the `details-command` snapshot pins the command surface and the fixed replay together.
|
||||
- The selector applies one dimension per confirm; a combined change still needs the argument form. Enter on the already-current row re-applies it idempotently and repeats its notice.
|
||||
- Transcript rebuilds no longer lose assistant messages when a step carries more than one `assistant/message` event; the `details-command` snapshot pins the argument surface and the fixed replay, and `details-selector` pins the open selector with its current-state markers.
|
||||
@@ -10,7 +10,7 @@ TUI 的 transcript(文本记录)细节状态——工具卡片可见性(`c
|
||||
|
||||
## Decision
|
||||
|
||||
`dsh-tui` 在其他 agent 作用域命令旁注册 `/details`。裸 `/details` 用一条通知报告当前状态。参数直接命名目标状态:`collapsed|expanded|hidden` 让工具卡片跳到该阶段,`reasoning on|off` 设置 reasoning 显示,裸 `reasoning` 切换它,且指令可在一次调用中组合。未知 token 返回携带用法行的命令错误。命令改动的是与快捷键相同的闭包状态,重构后循环与切换成为 `setToolsVisibility`/`setReasoning` 之上的薄封装;快捷键及其通知保持不变。
|
||||
`dsh-tui` 在其他 agent 作用域命令旁注册 `/details`。裸 `/details` 打开 `DetailsDialog`:一个居中的键盘选择器,列出五个细节状态——三个工具卡片阶段与 reasoning 显示/隐藏——预选当前阶段并标记两个当前值,Enter 应用高亮状态并关闭,Esc 或 Ctrl+C 取消;其宽度由配置键 `detailsDialogWidth` 决定,选择器打开时再次执行 `/details` 会替换它,与 `/model` 浮层一致。参数直接命名目标状态:`collapsed|expanded|hidden` 让工具卡片跳到该阶段,`reasoning on|off` 设置 reasoning 显示,裸 `reasoning` 切换它,且指令可在一次调用中组合。未知 token 返回携带用法行的命令错误。每个入口改动的都是与快捷键相同的闭包状态,重构后循环与切换成为 `setToolsVisibility`/`setReasoning` 之上的薄封装;快捷键及其通知保持不变。
|
||||
|
||||
组合调用先应用 reasoning 再应用可见性,因为 `setReasoning` 会从会话事件重建 transcript,而重建会丢弃非持久的通知组件;若最后才应用它,会抹掉刚追加的可见性通知。
|
||||
|
||||
@@ -18,7 +18,9 @@ reasoning 重建暴露了一个重放缺陷,本变更在 `renderEvent` 中修
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**裸 `/details` 像 Ctrl+O 一样循环。** 否决:命令相对快捷键的价值在于命名绝对状态;循环命令只是按键更多的快捷键,裸调用作为状态报告更有用。
|
||||
**裸 `/details` 像 Ctrl+O 一样循环。** 否决:命令相对快捷键的价值在于命名绝对状态;循环命令只是按键更多的快捷键,裸调用作为选择器更有用——它在展示当前状态的同时提供所有目标。
|
||||
|
||||
**裸 `/details` 仅输出文本状态报告。** 首版如此实现,后被选择器取代:报告回答了“我在哪”,但改变任何东西仍需第二次、拼写参数的调用;选择器展示同样的状态并在一次交互中应用变更。文本语法保留给脚本、肌肉记忆和两维组合变更。
|
||||
|
||||
**拆分 `/tools` 与 `/reasoning` 两个命令。** 否决:两个维度同属一个展示关注点(“transcript 显示多少细节”),单一命令让注册表与 `/help` 列表更小,同时允许一次组合调用。
|
||||
|
||||
@@ -26,6 +28,7 @@ reasoning 重建暴露了一个重放缺陷,本变更在 `renderEvent` 中修
|
||||
|
||||
## Consequences
|
||||
|
||||
- 用户可以跳到任意细节模式、一次设置两个维度并查询状态——包括在拦截 Ctrl+O/Ctrl+R 的终端上。
|
||||
- 用户可以跳到任意细节模式、一次设置两个维度,并在选择器中看到当前状态——包括在拦截 Ctrl+O/Ctrl+R 的终端上。
|
||||
- 解析器接受无序 token,因此 `/details reasoning expanded` 会切换 reasoning 并展开卡片;每个维度以最后一个指令为准。这一宽松是刻意的,并记录在 README 中。
|
||||
- 当一个步骤携带多条 `assistant/message` 事件时,transcript 重建不再丢失 assistant 消息;`details-command` 快照同时固定命令表面与修复后的重放。
|
||||
- 选择器每次确认只应用一个维度;组合变更仍需参数形式。在已是当前值的行上按 Enter 会幂等地重新应用并重复其通知。
|
||||
- 当一个步骤携带多条 `assistant/message` 事件时,transcript 重建不再丢失 assistant 消息;`details-command` 快照固定参数表面与修复后的重放,`details-selector` 固定带当前值标记的打开选择器。
|
||||
@@ -2034,6 +2034,8 @@ export interface TuiConfig {
|
||||
questionDialogMaxHeight?: number
|
||||
/** Model-selector width in terminal columns. */
|
||||
modelDialogWidth?: number
|
||||
/** Transcript-details selector width in terminal columns. */
|
||||
detailsDialogWidth?: number
|
||||
/** Model-selector maximum height in terminal rows. */
|
||||
modelDialogMaxHeight?: number
|
||||
/** Maximum fuzzy file candidates displayed for one `@` query. */
|
||||
@@ -2067,7 +2069,7 @@ export interface TuiThemeConfig {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/ui/tui/src/config.ts:117`](../packages/ui/tui/src/config.ts)
|
||||
Source: [`packages/ui/tui/src/config.ts:121`](../packages/ui/tui/src/config.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-typert-loader`
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import type {
|
||||
import type { AskUserQuestionItem } from '@deepseek-ai/dsh-user-interaction'
|
||||
import { BRACKETED_PASTE_END, BRACKETED_PASTE_START, displayText, sanitizePastedText } from './text.ts'
|
||||
import { dialogSelectTheme, type Palette } from './theme.ts'
|
||||
import type { ToolCardVisibility } from './transcript.ts'
|
||||
import {
|
||||
renderTuiPromptTemplate,
|
||||
type TuiPromptTemplateToken,
|
||||
@@ -432,6 +433,63 @@ export class ModelDialog implements Component {
|
||||
}
|
||||
}
|
||||
|
||||
/** One transcript-detail state the details selector applies on confirm. */
|
||||
export type DetailsSelection =
|
||||
| { readonly kind: 'tools'; readonly visibility: ToolCardVisibility }
|
||||
| { readonly kind: 'reasoning'; readonly show: boolean }
|
||||
|
||||
/**
|
||||
* Keyboard selector over the transcript detail states: the three tool-card
|
||||
* visibility phases and reasoning-block display. Enter applies the highlighted
|
||||
* state and closes; Esc or Ctrl+C closes without changing anything.
|
||||
*/
|
||||
export class DetailsDialog implements Component {
|
||||
private readonly list: SelectList
|
||||
|
||||
constructor(
|
||||
visibility: ToolCardVisibility,
|
||||
showReasoning: boolean,
|
||||
private readonly palette: Palette,
|
||||
done: (selection: DetailsSelection) => void,
|
||||
private readonly cancel: () => void,
|
||||
) {
|
||||
const current = (isCurrent: boolean): string => isCurrent ? ' — current' : ''
|
||||
const items: SelectItem[] = [
|
||||
{ value: 'collapsed', label: 'Tool cards · collapsed', description: `head/tail preview${current(visibility === 'collapsed')}` },
|
||||
{ value: 'expanded', label: 'Tool cards · expanded', description: `full bodies${current(visibility === 'expanded')}` },
|
||||
{ value: 'hidden', label: 'Tool cards · hidden', description: `conversation only${current(visibility === 'hidden')}` },
|
||||
{ value: 'reasoning-shown', label: 'Reasoning · shown', description: `show reasoning blocks${current(showReasoning)}` },
|
||||
{ value: 'reasoning-hidden', label: 'Reasoning · hidden', description: `omit reasoning blocks${current(!showReasoning)}` },
|
||||
]
|
||||
this.list = new SelectList(items, items.length, dialogSelectTheme(palette))
|
||||
this.list.setSelectedIndex(items.findIndex(item => item.value === visibility))
|
||||
this.list.onSelect = (item) => {
|
||||
done(item.value === 'reasoning-shown' || item.value === 'reasoning-hidden'
|
||||
? { kind: 'reasoning', show: item.value === 'reasoning-shown' }
|
||||
: { kind: 'tools', visibility: item.value as ToolCardVisibility })
|
||||
}
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
this.list.invalidate()
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) this.cancel()
|
||||
else this.list.handleInput(data)
|
||||
this.invalidate()
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
const innerWidth = Math.max(1, width - 4)
|
||||
return renderDialog('Transcript details', [
|
||||
...this.list.render(innerWidth),
|
||||
'',
|
||||
this.palette.dim('↑/↓ move • Enter apply • Esc cancel'),
|
||||
], width, this.palette)
|
||||
}
|
||||
}
|
||||
|
||||
/** The provider/model route recovered from a resume candidate's log. */
|
||||
export interface ResumeRoute {
|
||||
provider: string
|
||||
|
||||
@@ -46,6 +46,8 @@ export interface TuiConfig {
|
||||
questionDialogMaxHeight?: number
|
||||
/** Model-selector width in terminal columns. */
|
||||
modelDialogWidth?: number
|
||||
/** Transcript-details selector width in terminal columns. */
|
||||
detailsDialogWidth?: number
|
||||
/** Model-selector maximum height in terminal rows. */
|
||||
modelDialogMaxHeight?: number
|
||||
/** Maximum fuzzy file candidates displayed for one `@` query. */
|
||||
@@ -70,6 +72,7 @@ const maxResumeOptionsSchema = z.number().step(1).min(1).default(8)
|
||||
const questionDialogWidthSchema = z.number().step(1).min(20).default(200)
|
||||
const questionDialogMaxHeightSchema = z.number().step(1).min(6).default(20)
|
||||
const modelDialogWidthSchema = z.number().step(1).min(20).default(76)
|
||||
const detailsDialogWidthSchema = z.number().step(1).min(20).default(72)
|
||||
const modelDialogMaxHeightSchema = z.number().step(1).min(6).default(20)
|
||||
const fileSearchMaxResultsSchema = z.number().step(1).min(1).default(DEFAULT_FILE_SEARCH_MAX_RESULTS)
|
||||
const fileSearchMaxEntriesSchema = z.number().step(1).min(1).default(DEFAULT_FILE_SEARCH_MAX_ENTRIES)
|
||||
@@ -102,6 +105,7 @@ const tuiConfigSchemaFields = {
|
||||
questionDialogMaxHeight: questionDialogMaxHeightSchema,
|
||||
modelDialogWidth: modelDialogWidthSchema,
|
||||
modelDialogMaxHeight: modelDialogMaxHeightSchema,
|
||||
detailsDialogWidth: detailsDialogWidthSchema,
|
||||
fileSearchMaxResults: fileSearchMaxResultsSchema,
|
||||
fileSearchMaxEntries: fileSearchMaxEntriesSchema,
|
||||
fileSearchExcludedDirectories: fileSearchExcludedDirectoriesSchema,
|
||||
@@ -142,6 +146,7 @@ export const Config: z<Config> = z.object({
|
||||
questionDialogMaxHeight: tuiConfigSchemaFields.questionDialogMaxHeight,
|
||||
modelDialogWidth: tuiConfigSchemaFields.modelDialogWidth,
|
||||
modelDialogMaxHeight: tuiConfigSchemaFields.modelDialogMaxHeight,
|
||||
detailsDialogWidth: tuiConfigSchemaFields.detailsDialogWidth,
|
||||
fileSearchMaxResults: tuiConfigSchemaFields.fileSearchMaxResults,
|
||||
fileSearchMaxEntries: tuiConfigSchemaFields.fileSearchMaxEntries,
|
||||
fileSearchExcludedDirectories: tuiConfigSchemaFields.fileSearchExcludedDirectories,
|
||||
@@ -171,6 +176,7 @@ export interface ResolvedTuiConfig {
|
||||
questionDialogMaxHeight: number
|
||||
modelDialogWidth: number
|
||||
modelDialogMaxHeight: number
|
||||
detailsDialogWidth: number
|
||||
fileSearchMaxResults: number
|
||||
fileSearchMaxEntries: number
|
||||
fileSearchExcludedDirectories: string[]
|
||||
@@ -196,6 +202,7 @@ export function resolveTuiConfig(config: TuiConfig | undefined): ResolvedTuiConf
|
||||
questionDialogMaxHeight: config?.questionDialogMaxHeight ?? 20,
|
||||
modelDialogWidth: config?.modelDialogWidth ?? 76,
|
||||
modelDialogMaxHeight: config?.modelDialogMaxHeight ?? 20,
|
||||
detailsDialogWidth: config?.detailsDialogWidth ?? 72,
|
||||
fileSearchMaxResults: config?.fileSearchMaxResults ?? DEFAULT_FILE_SEARCH_MAX_RESULTS,
|
||||
fileSearchMaxEntries: config?.fileSearchMaxEntries ?? DEFAULT_FILE_SEARCH_MAX_ENTRIES,
|
||||
fileSearchExcludedDirectories: [...(config?.fileSearchExcludedDirectories ?? DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES)],
|
||||
|
||||
@@ -104,6 +104,7 @@ import {
|
||||
} from './components/transcript.ts'
|
||||
import {
|
||||
compactTargetLabel,
|
||||
DetailsDialog,
|
||||
diagnosticMeter,
|
||||
formatDiagnosticCount,
|
||||
formatDiagnosticNumber,
|
||||
@@ -112,6 +113,7 @@ import {
|
||||
StatusCardComponent,
|
||||
PromptContextComponent,
|
||||
targetLabel,
|
||||
type DetailsSelection,
|
||||
type StatusCardRow,
|
||||
} from './components/dialogs.ts'
|
||||
import {
|
||||
@@ -1037,12 +1039,38 @@ export function createTuiChat(
|
||||
|
||||
const toggleReasoning = (): void => { setReasoning(!showReasoning) }
|
||||
|
||||
// The selector and the argument grammar mutate the same closure state the
|
||||
// Ctrl+O cycle and Ctrl+R toggle drive, so every entry converges.
|
||||
let detailsOverlay: TuiOverlaySession | undefined
|
||||
const showDetailsSelector = (): void => {
|
||||
void detailsOverlay?.close()
|
||||
const session = overlayManager.open({
|
||||
create: () => new DetailsDialog(
|
||||
toolsVisibility,
|
||||
showReasoning,
|
||||
palette,
|
||||
(selection: DetailsSelection) => {
|
||||
void session.close()
|
||||
if (selection.kind === 'reasoning') setReasoning(selection.show)
|
||||
else setToolsVisibility(selection.visibility)
|
||||
},
|
||||
() => { void session.close() },
|
||||
),
|
||||
options: { width: resolved.detailsDialogWidth, anchor: 'center', margin: 1 },
|
||||
})
|
||||
detailsOverlay = session
|
||||
void session.closed.then(() => {
|
||||
if (detailsOverlay === session) detailsOverlay = undefined
|
||||
})
|
||||
requestRender()
|
||||
}
|
||||
|
||||
// `/details` names the same transcript-detail state the Ctrl+O cycle and
|
||||
// Ctrl+R toggle mutate, so a user can jump to a mode without cycling.
|
||||
const runDetails = (rawInput: string): CommandResult => {
|
||||
const tokens = rawInput.split(/\s+/u).filter(token => token !== '')
|
||||
if (tokens.length === 0) {
|
||||
appendNotice(`Tool and context cards ${toolsVisibility}; reasoning blocks ${showReasoning ? 'shown' : 'hidden'}.`)
|
||||
showDetailsSelector()
|
||||
return { kind: 'success' }
|
||||
}
|
||||
let visibility: ToolCardVisibility | undefined
|
||||
@@ -1255,7 +1283,7 @@ export function createTuiChat(
|
||||
})
|
||||
commandCtx.commands.register({
|
||||
name: 'details',
|
||||
description: 'Show or set tool-card visibility and reasoning display',
|
||||
description: 'Select tool-card visibility and reasoning display',
|
||||
input: { hint: '[collapsed|expanded|hidden] [reasoning [on|off]]' },
|
||||
handler: ({ rawInput }) => runDetails(rawInput),
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
terminal 100x40 buffer=normal length=40 base=0 viewport=0
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "DSH snapshot"
|
||||
cursor hidden column=7 viewportRow=19 bufferRow=19
|
||||
cursor hidden column=7 viewportRow=17 bufferRow=17
|
||||
buffer
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
@@ -29,17 +29,14 @@ buffer
|
||||
14| "Tool cards hidden. "
|
||||
style 0-17 dim
|
||||
15| <blank>
|
||||
16| "Tool and context cards hidden; reasoning blocks hidden. "
|
||||
style 0-54 dim
|
||||
17| <blank>
|
||||
18| "/workspace/project (tui-staging) deepseek-v4-flash ↑0 ↓0 0% context"
|
||||
16| "/workspace/project (tui-staging) deepseek-v4-flash ↑0 ↓0 0% context"
|
||||
style 0-17 fg=bright-magenta bold
|
||||
style 18-31 dim
|
||||
style 34-50 dim
|
||||
style 53-57 dim
|
||||
style 60-69 dim
|
||||
19| " dsh > "
|
||||
17| " dsh > "
|
||||
style 1-3 fg=bright-magenta bold
|
||||
style 5-6 dim
|
||||
style 7-7 inverse
|
||||
20-39| <blank>
|
||||
18-39| <blank>
|
||||
@@ -0,0 +1,66 @@
|
||||
terminal 100x40 buffer=normal length=40 base=0 viewport=0
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "DSH snapshot"
|
||||
cursor hidden column=0 viewportRow=39 bufferRow=39
|
||||
buffer
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
1| " Snapshot agent ready."
|
||||
style 1-21 dim
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
5| "Running the check now. "
|
||||
6| "Model wait 0.0s "
|
||||
style 0-14 dim
|
||||
7| <blank>
|
||||
8| "You "
|
||||
style 0-2 fg=bright-magenta bold underline
|
||||
9| "Inspect the renderer. "
|
||||
10| "Model wait 0.0s · Completed 2026-07-30 18:00:00 "
|
||||
style 0-46 dim
|
||||
11| <blank>
|
||||
12| "Reasoning blocks hidden. "
|
||||
style 0-23 dim
|
||||
13| <blank>
|
||||
14| "Tool cards hidden. "
|
||||
style 0-17 dim
|
||||
15| " ╭ Transcript details ──────────────────────────────────────────────────╮ "
|
||||
style 14-85 fg=bright-magenta
|
||||
16| "/workspace/pro│ Tool cards · collapsed head/tail preview │ "
|
||||
style 0-13 fg=bright-magenta bold
|
||||
style 14-14 fg=bright-magenta
|
||||
style 40-66 dim
|
||||
style 85-85 fg=bright-magenta
|
||||
17| " dsh > │ Tool cards · expanded full bodies │ "
|
||||
style 1-3 fg=bright-magenta bold
|
||||
style 5-6 dim
|
||||
style 7-7 inverse
|
||||
style 14-14 fg=bright-magenta
|
||||
style 39-60 dim
|
||||
style 85-85 fg=bright-magenta
|
||||
18| " │ → Tool cards · hidden conversation only — current │ "
|
||||
style 14-14 fg=bright-magenta
|
||||
style 16-76 fg=bright-magenta inverse
|
||||
style 85-85 fg=bright-magenta
|
||||
19| " │ Reasoning · shown show reasoning blocks │ "
|
||||
style 14-14 fg=bright-magenta
|
||||
style 35-70 dim
|
||||
style 85-85 fg=bright-magenta
|
||||
20| " │ Reasoning · hidden omit reasoning blocks — current │ "
|
||||
style 14-14 fg=bright-magenta
|
||||
style 36-80 dim
|
||||
style 85-85 fg=bright-magenta
|
||||
21| " │ │ "
|
||||
style 14-14 fg=bright-magenta
|
||||
style 85-85 fg=bright-magenta
|
||||
22| " │ ↑/↓ move • Enter apply • Esc cancel │ "
|
||||
style 14-14 fg=bright-magenta
|
||||
style 16-50 dim
|
||||
style 85-85 fg=bright-magenta
|
||||
23| " ╰──────────────────────────────────────────────────────────────────────╯ "
|
||||
style 14-85 fg=bright-magenta
|
||||
24-39| <blank>
|
||||
@@ -29,10 +29,10 @@ buffer
|
||||
12| " "
|
||||
13| "/clear — Clear the transcript view (session history is unchanged) "
|
||||
style 0-64 dim
|
||||
14| "/details [collapsed|expanded|hidden] [reasoning [on|off]] — Show or set tool-card visibility"
|
||||
14| "/details [collapsed|expanded|hidden] [reasoning [on|off]] — Select tool-card visibility and "
|
||||
style 0-91 dim
|
||||
15| "and reasoning display "
|
||||
style 0-20 dim
|
||||
15| "reasoning display "
|
||||
style 0-16 dim
|
||||
16| "/exit — Exit after the active turn reaches idle "
|
||||
style 0-46 dim
|
||||
17| "/help — Show keyboard shortcuts and commands "
|
||||
|
||||
@@ -29,10 +29,10 @@ buffer
|
||||
12| " "
|
||||
13| "/clear — Clear the transcript view (session history is unchanged) "
|
||||
style 0-64 dim
|
||||
14| "/details [collapsed|expanded|hidden] [reasoning [on|off]] — Show or set tool-card visibility"
|
||||
14| "/details [collapsed|expanded|hidden] [reasoning [on|off]] — Select tool-card visibility and "
|
||||
style 0-91 dim
|
||||
15| "and reasoning display "
|
||||
style 0-20 dim
|
||||
15| "reasoning display "
|
||||
style 0-16 dim
|
||||
16| "/exit — Exit after the active turn reaches idle "
|
||||
style 0-46 dim
|
||||
17| "/help — Show keyboard shortcuts and commands "
|
||||
|
||||
@@ -46,6 +46,7 @@ const CHECKPOINTS = [
|
||||
'advanced-cards-expanded',
|
||||
'tool-cards-hidden-folded',
|
||||
'details-command',
|
||||
'details-selector',
|
||||
'untrusted-controls',
|
||||
'question-dialog',
|
||||
'question-dialog-single-option',
|
||||
@@ -661,11 +662,14 @@ describe('TUI terminal-state snapshots', () => {
|
||||
harness.terminal.send('/details hidden reasoning off')
|
||||
harness.terminal.send('\r')
|
||||
})
|
||||
await checkpoint('details-command', harness.terminal, { includeScrollback: true })
|
||||
// Bare /details opens the selector, preselecting and marking the current
|
||||
// hidden/reasoning-off state.
|
||||
await renderAfter(harness, () => {
|
||||
harness.terminal.send('/details')
|
||||
harness.terminal.send('\r')
|
||||
})
|
||||
await checkpoint('details-command', harness.terminal, { includeScrollback: true })
|
||||
await checkpoint('details-selector', harness.terminal, { includeScrollback: true })
|
||||
nowSpy.mockRestore()
|
||||
await disposeSnapshot(harness)
|
||||
})
|
||||
|
||||
@@ -186,6 +186,7 @@ describe('TUI config', () => {
|
||||
questionDialogMaxHeight: 20,
|
||||
modelDialogWidth: 76,
|
||||
modelDialogMaxHeight: 20,
|
||||
detailsDialogWidth: 72,
|
||||
fileSearchMaxResults: 20,
|
||||
fileSearchMaxEntries: 10_000,
|
||||
fileSearchExcludedDirectories: ['.git', 'node_modules'],
|
||||
@@ -210,6 +211,7 @@ describe('TUI config', () => {
|
||||
questionDialogMaxHeight: 14,
|
||||
modelDialogWidth: 64,
|
||||
modelDialogMaxHeight: 16,
|
||||
detailsDialogWidth: 44,
|
||||
fileSearchMaxResults: 7,
|
||||
fileSearchMaxEntries: 123,
|
||||
fileSearchExcludedDirectories: ['.git', 'generated'],
|
||||
@@ -226,6 +228,7 @@ describe('TUI config', () => {
|
||||
questionDialogMaxHeight: 14,
|
||||
modelDialogWidth: 64,
|
||||
modelDialogMaxHeight: 16,
|
||||
detailsDialogWidth: 44,
|
||||
fileSearchMaxResults: 7,
|
||||
fileSearchMaxEntries: 123,
|
||||
fileSearchExcludedDirectories: ['.git', 'generated'],
|
||||
@@ -2510,7 +2513,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('/details reports and sets card visibility and reasoning display', async () => {
|
||||
it('/details sets card visibility and reasoning display from arguments', async () => {
|
||||
const result = await setup()
|
||||
const run = async (line: string): Promise<void> => {
|
||||
result.terminal.send(line)
|
||||
@@ -2518,9 +2521,6 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
await tick()
|
||||
}
|
||||
|
||||
await run('/details')
|
||||
expect(result.terminal.output).toContain('Tool and context cards collapsed; reasoning blocks shown.')
|
||||
|
||||
await run('/details hidden')
|
||||
expect(result.terminal.output).toContain('Tool cards hidden.')
|
||||
|
||||
@@ -2531,11 +2531,12 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
await run('/details reasoning on')
|
||||
expect(result.terminal.output).toContain('Reasoning blocks shown.')
|
||||
|
||||
// Bare `reasoning` toggles: shown -> hidden, confirmed by the status line.
|
||||
// Bare `reasoning` toggles: shown -> hidden.
|
||||
const toggleOutput = result.terminal.output.length
|
||||
await run('/details reasoning')
|
||||
expect(result.terminal.output.slice(toggleOutput)).toContain('Reasoning blocks hidden.')
|
||||
await run('/details collapsed')
|
||||
await run('/details')
|
||||
expect(result.terminal.output).toContain('Tool and context cards collapsed; reasoning blocks hidden.')
|
||||
expect(result.terminal.output.slice(toggleOutput)).toContain('Tool and context cards collapsed.')
|
||||
|
||||
await run('/details bogus')
|
||||
expect(result.terminal.output).toContain('Unknown /details argument "bogus"')
|
||||
@@ -2543,6 +2544,59 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('bare /details opens the transcript-details selector and applies the confirmed state', async () => {
|
||||
const result = await setup()
|
||||
const open = async (): Promise<number> => {
|
||||
const from = result.terminal.output.length
|
||||
result.terminal.send('/details')
|
||||
result.terminal.send('\r')
|
||||
await vi.waitFor(() => { expect(result.terminal.output.slice(from)).toContain('Transcript details') })
|
||||
return from
|
||||
}
|
||||
|
||||
await open()
|
||||
expect(result.terminal.output).toContain('Tool cards · collapsed')
|
||||
expect(result.terminal.output).toContain('head/tail preview — current')
|
||||
expect(result.terminal.output).toContain('show reasoning blocks — current')
|
||||
|
||||
// A second /details while the selector is open replaces the overlay
|
||||
// instead of stacking a second one behind it.
|
||||
await result.ctx.commands.execute(result.agent, '/details', new AbortController().signal)
|
||||
await tick()
|
||||
|
||||
// Esc cancels without touching the state.
|
||||
const cancelOutput = result.terminal.output.length
|
||||
result.terminal.send('\x1b')
|
||||
await tick()
|
||||
expect(result.terminal.output.slice(cancelOutput)).not.toContain('Tool and context cards')
|
||||
|
||||
// Enter on the next visibility row applies it and closes.
|
||||
await open()
|
||||
result.terminal.send('\x1b[B')
|
||||
result.terminal.send('\r')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Tool and context cards expanded.')
|
||||
|
||||
// The reopened selector preselects the current phase and marks it.
|
||||
const reopened = await open()
|
||||
expect(result.terminal.output.slice(reopened)).toContain('full bodies — current')
|
||||
result.terminal.send('\x1b[B')
|
||||
result.terminal.send('\x1b[B')
|
||||
result.terminal.send('\x1b[B')
|
||||
result.terminal.send('\r')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Reasoning blocks hidden.')
|
||||
|
||||
// Ctrl+C also cancels.
|
||||
const ctrlCOutput = result.terminal.output.length
|
||||
await open()
|
||||
result.terminal.send('\x03')
|
||||
await tick()
|
||||
expect(result.terminal.output.slice(ctrlCOutput)).not.toContain('Reasoning blocks shown.')
|
||||
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('sends, steers, handles commands, global keys, and disposed-agent input', async () => {
|
||||
const result = await setup()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user