docs: describe one-line docks and consistent transcript controls

This commit is contained in:
Teknium
2026-09-08 03:36:32 -07:00
parent 0a3b7fdce2
commit 3863d13440
11 changed files with 109 additions and 43 deletions
+27 -13
View File
@@ -27,34 +27,48 @@ it('keeps collapsed live chrome to one row without losing count or restore contr
expect(text).toContain(`${rows.running} live agents`)
expect(text).toContain('Ctrl+T expand')
expect(text).toContain('F7 restore')
expect(renderToScreen(<AgentsPanelView cols={cols} {...rows} t={DEFAULT_THEME} />, cols).height).toBeGreaterThan(view.height)
expect(renderToScreen(<AgentsPanelView cols={cols} {...rows} t={DEFAULT_THEME} />, cols).height).toBeGreaterThan(
view.height
)
}
})
it('opens the selected live transcript on Enter while details remain independently accessible', async () => {
patchUiState({ sid: 'owner' })
applyAgentSnapshot('owner', { subagents: [{ subagent_id: 'child', goal: 'Inspect ownership', status: 'running' }], delegations: [] })
applyAgentSnapshot('owner', {
subagents: [{ subagent_id: 'child', goal: 'Inspect ownership', status: 'running' }],
delegations: []
})
const request = vi.fn(async (method: string) => method === 'subagent.tail'
? { available: true, text: 'CHILD_TOOL_OUTPUT', truncated: false }
: {})
const request = vi.fn(async (method: string) =>
method === 'subagent.tail' ? { available: true, text: 'CHILD_TOOL_OUTPUT', truncated: false } : {}
)
const stdout = Object.assign(new PassThrough(), { columns: 80, rows: 20, isTTY: false })
const stdin = Object.assign(new PassThrough(), { isTTY: true, setRawMode: () => {}, ref: () => {}, unref: () => {} })
let output = ''
stdout.on('data', chunk => { output += stripAnsi(chunk.toString()) })
const view = renderSync(<Box height={20}><AgentsOverlay gw={{ request } as unknown as GatewayClient} onClose={() => {}} t={DEFAULT_THEME} /></Box>, {
stdout: stdout as unknown as NodeJS.WriteStream,
stdin: stdin as unknown as NodeJS.ReadStream,
stderr: new PassThrough() as unknown as NodeJS.WriteStream,
patchConsole: false
stdout.on('data', chunk => {
output += stripAnsi(chunk.toString())
})
const view = renderSync(
<Box height={20}>
<AgentsOverlay gw={{ request } as unknown as GatewayClient} onClose={() => {}} t={DEFAULT_THEME} />
</Box>,
{
stdout: stdout as unknown as NodeJS.WriteStream,
stdin: stdin as unknown as NodeJS.ReadStream,
stderr: new PassThrough() as unknown as NodeJS.WriteStream,
patchConsole: false
}
)
try {
await vi.waitFor(() => expect(output).toContain('Inspect ownership'))
stdin.write('\r')
await vi.waitFor(() => expect(request).toHaveBeenCalledWith('subagent.tail', { session_id: 'owner', subagent_id: 'child' }))
await vi.waitFor(() =>
expect(request).toHaveBeenCalledWith('subagent.tail', { session_id: 'owner', subagent_id: 'child' })
)
await vi.waitFor(() => expect(output).toContain('CHILD_TOOL_OUTPUT'))
output = ''
stdin.write('d')
+16 -5
View File
@@ -772,9 +772,13 @@ export function AgentsOverlay({ gw, initialHistoryIndex = 0, onClose, t }: Agent
return setMode('steer')
}
if (ch === 't' && !key.ctrl && selected) {return setMode('tail')}
if (ch === 't' && !key.ctrl && selected) {
return setMode('tail')
}
if (ch === 'd' && !key.ctrl && selected) {return setMode('detail')}
if (ch === 'd' && !key.ctrl && selected) {
return setMode('detail')
}
if (ch === 'q') {
return closeWithCleanup()
@@ -980,12 +984,19 @@ export function AgentsOverlay({ gw, initialHistoryIndex = 0, onClose, t }: Agent
)}
<Box flexDirection="column" flexShrink={0} marginTop={1}>
<Text color={t.color.accent} wrap="truncate-end">{replayMode ? 'Enter/d detail' : 'Enter/t tail · d detail'} · e steer · x stop · Esc back</Text>
{flash ? <Text color={t.color.accent} wrap="truncate-end">{flash}</Text> : null}
<Text color={t.color.accent} wrap="truncate-end">
{replayMode ? 'Enter/d detail' : 'Enter/t tail · d detail'} · e steer · x stop · Esc back
</Text>
{flash ? (
<Text color={t.color.accent} wrap="truncate-end">
{flash}
</Text>
) : null}
{mode === 'list' ? (
<Text color={t.color.muted} wrap="truncate-end">
↑↓/jk move · g/G top/bottom · {replayMode ? 'Enter/→ detail' : 'Enter tail · d/→ detail'}{controlsHint} · s sort:{SORT_LABEL[sort]} · f filter:
↑↓/jk move · g/G top/bottom · {replayMode ? 'Enter/→ detail' : 'Enter tail · d/→ detail'}
{controlsHint} · s sort:{SORT_LABEL[sort]} · f filter:
{FILTER_LABEL[filter]}
{history.length > 0 ? ` · [ / ] history ${historyIndex}/${history.length}` : ''}
{' · q close'}
+31 -14
View File
@@ -11,8 +11,17 @@ import { fmtDuration } from '../lib/subagentTree.js'
import { compactPreview } from '../lib/text.js'
import type { Theme } from '../theme.js'
export function AgentsPanelView({ collapsed = false, cols, hidden, rows, running, t }: AgentRows & { collapsed?: boolean; cols: number; t: Theme }) {
if (!running) {return null}
export function AgentsPanelView({
collapsed = false,
cols,
hidden,
rows,
running,
t
}: AgentRows & { collapsed?: boolean; cols: number; t: Theme }) {
if (!running) {
return null
}
const summary = `▸ ${running} live agents`
const hints = ' · Ctrl+T expand · F7 restore'
@@ -27,18 +36,21 @@ export function AgentsPanelView({ collapsed = false, cols, hidden, rows, running
width={cols}
>
<Text bold color={t.color.accent} wrap="truncate-end">
{collapsed ? summary + activity + hints : `▾ ${running} live agents${hidden ? ` · +${hidden} more` : ''} · Ctrl+T expand · F7 collapse`}
{collapsed
? summary + activity + hints
: `▾ ${running} live agents${hidden ? ` · +${hidden} more` : ''} · Ctrl+T expand · F7 collapse`}
</Text>
{!collapsed && rows.map(row => (
<Box flexDirection="column" key={row.key}>
<Text wrap="truncate-end">
<Text color={statusGlyph(row.status, t).color}>{statusGlyph(row.status, t).glyph} </Text>
<Text color={t.color.text}>{compactPreview(row.goal, Math.max(8, cols - 18))}</Text>
<Text color={t.color.muted}> {row.elapsedSeconds == null ? '' : fmtDuration(row.elapsedSeconds)}</Text>
</Text>
<Text color={t.color.muted} wrap="truncate-end">{` ↳ ${compactPreview(row.detail, cols - 4)}`}</Text>
</Box>
))}
{!collapsed &&
rows.map(row => (
<Box flexDirection="column" key={row.key}>
<Text wrap="truncate-end">
<Text color={statusGlyph(row.status, t).color}>{statusGlyph(row.status, t).glyph} </Text>
<Text color={t.color.text}>{compactPreview(row.goal, Math.max(8, cols - 18))}</Text>
<Text color={t.color.muted}> {row.elapsedSeconds == null ? '' : fmtDuration(row.elapsedSeconds)}</Text>
</Text>
<Text color={t.color.muted} wrap="truncate-end">{` ↳ ${compactPreview(row.detail, cols - 4)}`}</Text>
</Box>
))}
</Box>
)
}
@@ -61,6 +73,11 @@ export function LiveAgentsPanel({ cols }: { cols: number }) {
}, [live])
return (
<AgentsPanelView collapsed={collapsed} cols={cols} {...buildAgentRows(subagents, [], now, dockRowLimit(stdout?.rows ?? 24))} t={theme} />
<AgentsPanelView
collapsed={collapsed}
cols={cols}
{...buildAgentRows(subagents, [], now, dockRowLimit(stdout?.rows ?? 24))}
t={theme}
/>
)
}
+12 -3
View File
@@ -277,7 +277,9 @@ const ComposerPane = memo(function ComposerPane({
composer,
cursorSnapshotRef,
status
}: Pick<AppLayoutProps, 'actions' | 'composer' | 'status'> & { cursorSnapshotRef: MutableRefObject<InputCursorSnapshot | null> }) {
}: Pick<AppLayoutProps, 'actions' | 'composer' | 'status'> & {
cursorSnapshotRef: MutableRefObject<InputCursorSnapshot | null>
}) {
const ui = useStore($uiState)
const isBlocked = useStore($isBlocked)
const sh = (composer.inputBuf[0] ?? composer.input).startsWith('!')
@@ -534,7 +536,9 @@ export const AppLayout = memo(function AppLayout({
const ui = useStore($uiState)
const cursorSnapshotRef = useRef<InputCursorSnapshot | null>(null)
useEffect(() => { cursorSnapshotRef.current = null }, [ui.sid])
useEffect(() => {
cursorSnapshotRef.current = null
}, [ui.sid])
// Inline mode skips AlternateScreen so the host terminal's native
// scrollback captures rows scrolled off the top; composer + progress
@@ -577,7 +581,12 @@ export const AppLayout = memo(function AppLayout({
</PerfPane>
<PerfPane id="composer">
<ComposerPane actions={actions} composer={composer} cursorSnapshotRef={cursorSnapshotRef} status={status} />
<ComposerPane
actions={actions}
composer={composer}
cursorSnapshotRef={cursorSnapshotRef}
status={status}
/>
</PerfPane>
{SHOW_FPS && (
+11 -4
View File
@@ -790,7 +790,9 @@ export function TextInput({
color,
focus = true
}: TextInputProps) {
const [cur, setCur] = useState(() => cursorSnapshotRef?.current?.value === value ? cursorSnapshotRef.current.cursor : value.length)
const [cur, setCur] = useState(() =>
cursorSnapshotRef?.current?.value === value ? cursorSnapshotRef.current.cursor : value.length
)
const [sel, setSel] = useState<null | { end: number; start: number }>(null)
const fwdDel = useFwdDelete(focus)
const termFocus = useTerminalFocus()
@@ -939,9 +941,14 @@ export function TextInput({
// The composer unmounts while full-screen monitors own input. Keep its
// insertion point with the shell, not with transient steer/secret inputs.
useEffect(() => () => {
if (cursorSnapshotRef) {cursorSnapshotRef.current = { cursor: curRef.current, value: vRef.current }}
}, [cursorSnapshotRef])
useEffect(
() => () => {
if (cursorSnapshotRef) {
cursorSnapshotRef.current = { cursor: curRef.current, value: vRef.current }
}
},
[cursorSnapshotRef]
)
useEffect(() => {
if (!focus) {
+1
View File
@@ -187,6 +187,7 @@ When resuming a previous session (`hermes -c` or `hermes --resume <id>`), a "Pre
| `Ctrl+S` | **Stash the prompt.** Parks the current draft and clears the composer so you can send something else first. Press `Ctrl+S` again on an empty composer to bring the draft back (cursor at the end, attached images restored). Repeated presses build a stack rather than overwriting, so an earlier draft is never silently lost — with two or more stashed, `Ctrl+S` opens a browse panel (`↑`/`↓` to navigate, `Enter` to restore, `D` to discard, `Esc` or `Ctrl+S` to close). A `📌 N` badge in the status bar shows how many drafts are parked. Multi-line drafts round-trip exactly, including blank lines. The stash lives in memory for the session only — nothing is written to disk, since drafts often contain secrets. |
| `Ctrl+C` | Interrupt agent (double-press within 2s to force exit) |
| `F6` | Open the full-screen live subagent monitor without losing the composer draft. The live dock appears automatically above the status bar; arrows select a worker, `Enter` shows its recent log, `s` steers, and `x` requests stop with confirmation. See [Monitoring subagents](/user-guide/features/delegation#monitoring-running-subagents-agents). |
| `F7` | Toggle the live subagent dock between its multi-row preview and a single summary line without moving composer focus. |
| `Ctrl+D` | Exit |
| `Ctrl+Z` | Suspend Hermes to background (Unix only). Run `fg` in the shell to resume. |
| `Tab` | Accept auto-suggestion (ghost text) or autocomplete slash commands |
@@ -392,11 +392,13 @@ The classic CLI, TUI, and Desktop automatically show live subagents above the co
| Surface | Expand and inspect | Control a selected worker |
|---|---|---|
| Classic CLI | **F6** opens the full-screen live roster; arrows select, **Enter** opens the transcript tail, **PgUp/PgDn** scroll | **s** opens a separate steering input; **x**, then **y** requests stop |
| TUI | **Ctrl+T** or `/agents` opens the full-height tree; **Enter** opens detail; **t** opens the live transcript tail | **e** opens steering; **x** stops the selected worker; **X** stops its subtree |
| TUI | **Ctrl+T** or `/agents` opens the full-height tree; **Enter/t** opens the live transcript tail; **d** opens rich detail (archived/replay Enter still opens detail) | **e** opens steering; **x** stops the selected worker; **X** stops its subtree |
| Desktop | Expand **Subagents** above the composer, then select a worker to inspect its activity and details | **Steer** queues guidance; **Stop** requests interruption for that worker |
Closing the terminal monitor returns to your existing composer draft. Steering uses its own input and acknowledges **queued**, not delivery: the child consumes guidance at a checkpoint. Stop does not interrupt unrelated siblings.
Press **F7** in the Classic CLI or TUI composer to toggle the dock between its multi-row preview and a single shaded summary line. The summary retains the live count and expand/restore hints, adding activity when space permits. Typing and sending remain available; opening and closing the monitor preserves your draft and insertion point. This is a local presentation choice, not a saved config change.
The live transcript tail is a bounded recent excerpt, not an unlimited conversation browser. A child leaving the live registry leaves the dock; completion messages and the TUI/Desktop history views remain the place to review finished work. Latest activity is an observation, not a percentage-complete estimate.
The classic CLI's `/agents` and `/tasks` commands still print a text summary; **F6** is the immediate interactive monitor, including while the parent is busy. See [TUI — Slash commands](/user-guide/tui#slash-commands).
+2 -1
View File
@@ -102,7 +102,8 @@ The directory must contain `dist/entry.js`.
Keybindings match the [Classic CLI](cli.md#keybindings) exactly. The only behavioral differences:
- **`Ctrl+T`** expands the automatic live-subagent dock into the full-height `/agents` roster. Select a worker to inspect details, press **`t`** for its recent transcript, **`e`** to steer, or **`x`** to stop it. The dock fits its row count to terminal height and preserves your composer draft. See [Monitoring subagents](/user-guide/features/delegation#monitoring-running-subagents-agents).
- **`Ctrl+T`** expands the automatic live-subagent dock into the full-height `/agents` roster. Select a worker and press **Enter** (or **`t`**) for its live transcript, **`d`** for rich details, **`e`** to steer, or **`x`** to stop it. The dock fits its row count to terminal height and preserves your composer draft. See [Monitoring subagents](/user-guide/features/delegation#monitoring-running-subagents-agents).
- **`F7`** toggles the live dock between its default preview and one summary line. This does not open the monitor or move composer focus; the choice lasts for this TUI process without changing config.
- **Mouse drag** highlights text with a uniform selection background.
- **`Cmd+V` / `Ctrl+V`** first tries normal text paste, then falls back to OSC52/native clipboard reads, and finally image attach when the clipboard or pasted payload resolves to an image.
- **`/terminal-setup`** installs local VS Code / Cursor / Windsurf terminal bindings for better `Cmd+Enter` and undo/redo parity on macOS.
@@ -103,6 +103,7 @@ hermes -w -z "Fix issue #123" # 在 worktree 中以单次查询模式运行
| `Ctrl+X Ctrl+E` | 外部编辑器的 Emacs 风格备用绑定(与 `Ctrl+G` 行为相同)。 |
| `Ctrl+C` | 中断 agent(2 秒内双击强制退出) |
| `F6` | 打开全屏实时子智能体监视器,保留输入草稿。方向键选择,`Enter` 查看近期日志,`s` 引导,`x` 请求停止并确认。 |
| `F7` | 将实时子智能体栏切换为单行摘要或恢复多行预览,不改变输入焦点。 |
| `Ctrl+D` | 退出 |
| `Ctrl+Z` | 将 Hermes 挂起到后台(仅 Unix)。在 shell 中运行 `fg` 恢复。 |
| `Tab` | 接受自动建议(ghost text)或自动补全斜杠命令 |
@@ -257,11 +257,13 @@ TUI 提供 `/agents` 浮层(别名 `/tasks`),将递归 `delegate_task` 扇
经典 CLI、TUI 和 Desktop 会在输入框上方自动显示正在运行的子智能体,包括总数、任务名称、已运行时间和最近活动。终端根据屏幕高度限制可见行数,并显示隐藏数量;Desktop 最多预览三个工作者。
- **经典 CLI:F6** 打开全屏实时列表;方向键选择,**Enter** 查看近期日志,**PgUp/PgDn** 滚动,**s** 输入引导,**x** 后按 **y** 确认停止。关闭后保留原有输入草稿。
- **TUI:Ctrl+T** 或 `/agents` 打开完整树状列表;**t** 查看实时日志,**e** 输入引导,**x** 停止选中的工作者,**X** 停止其子树。
- **TUI:Ctrl+T** 或 `/agents` 打开完整树状列表;**Enter/t** 查看实时日志,**d** 查看详情(历史回放中 Enter 仍打开详情),**e** 输入引导,**x** 停止选中的工作者,**X** 停止其子树。
- **Desktop:** 展开输入框上方的 **Subagents**,选择工作者查看详情并使用 **Steer** / **Stop**。
引导的“已排队”确认不代表子智能体已经读取;它会在检查点接收。日志预览只包含有大小限制的近期内容。工作者结束后离开实时列表,完成消息和已有历史视图仍可用于回顾。
在经典 CLI 和 TUI 中按 **F7**,可将实时栏折叠为单行摘要,再按一次恢复多行预览。单行保留运行数量和展开/恢复提示,空间允许时显示活动。输入和发送不受影响;关闭监视器后保留草稿及光标位置。此选项不写入配置。
经典 CLI 的 `/agents` 和 `/tasks` 仍打印文本摘要;父智能体忙碌时可直接按 **F6** 打开交互式监视器。参见 [TUI — 斜杠命令](/user-guide/tui#slash-commands)。
## 深度限制与嵌套编排 {#depth-limit-and-nested-orchestration}
@@ -85,7 +85,8 @@ hermes --tui
快捷键与 [Classic CLI](cli.md#keybindings) 完全一致。仅有以下行为差异:
- **`Ctrl+T`** — 将输入框上方的实时子智能体栏展开为完整 `/agents` 列表;**`t`** 查看近期日志,**`e`** 引导,**`x`** 停止选中的工作者。可见行数随终端高度调整,关闭后保留输入草稿。
- **`Ctrl+T`** — 将输入框上方的实时子智能体栏展开为完整 `/agents` 列表;**Enter/t** 查看实时日志,**`d`** 查看详细信息,**`e`** 引导,**`x`** 停止选中的工作者。可见行数随终端高度调整,关闭后保留输入草稿。
- **`F7`** — 在多行预览和单行摘要之间切换,保留输入焦点,不写入配置。
- **鼠标拖拽** — 以统一选区背景色高亮文本。
- **`Cmd+V` / `Ctrl+V`** — 优先尝试普通文本粘贴,然后回退到 OSC52/原生剪贴板读取,最后在剪贴板或粘贴内容解析为图片时进行图片附件操作。
- **`/terminal-setup`** — 安装本地 VS Code / Cursor / Windsurf 终端绑定,以在 macOS 上获得更好的 `Cmd+Enter` 和撤销/重做一致性。