feat(web): state the run state on the terminal card's prompt line
The terminal card showed no run state: a running command and a settled command that produced no output rendered the same prompt line, so whether a command was still running had to be inferred from the absence of output. Lead the prompt line with a StateDot in three of its states — the spinning ring while running, red for the same exit status that renders the status pill, green for a clean settle. That is the same indicator a tool row's leading icon carries, so a row and its own card cannot disagree about one command; the row/card agreement is pinned in the ui-conversation spec. StateDot is aria-hidden, so a visually hidden text label rides beside it, which is what the refreshed aria goldens now record. The e2e adds what jsdom cannot compute: the dot's color resolves to the green success token through the real theme stylesheet, and the dot precedes the prompt label in document order.
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 packages/client/ui-conversation/README.md
|
||||
README.md: 1c6912b05e259fa1f4a7096c3a2b82f9f67f5527
|
||||
README.zh.md: 157bfafd3a1157420acbb73861cd40d759228743
|
||||
README.md: 39d185014c658f490f0a3672ea3d7c99f30d8df2
|
||||
README.zh.md: 6b63631287bf420af0985a743f068f27b9c97873
|
||||
|
||||
@@ -10,7 +10,7 @@ The view ring IS a slot: the conversation registration declares the `'conversati
|
||||
|
||||
Generic tool rows classify the built-in bash, read, search, write, edit, and run_code names into dedicated visual variants. The filesystem variants render the edit icon and `Write · <path>` or `Edit · <path>` summary while retaining the shared row-to-details interaction. The code variant summarizes with the model-authored `description` and expands to the program itself; its logged sub-dispatches render as always-visible nested rows through the SAME keyed toolview hole (custom registrations and the GenericToolCard fallback apply to sub-rows unchanged), and the details panel resolves a selected sub-call id to its full logged args and complete output. Cordis lifecycle tools reuse those generic variants while presenting `Inspect`, `Mount temporary Plugin`, and `Unmount temporary Plugin` with a shared Cordis accent; mount keeps the code variant's expandable source rendering.
|
||||
|
||||
A tool call declaring the `terminal` render intent renders its command output inline, at both conversation render sites, through ui-primitives' `TerminalBlock`. `contract/terminal-card-model.ts` is the single derivation from the snapshot's `callView`/`resultView` pair, so the sites cannot disagree about a command, its cwd, or its exit status; it yields null — the generic path — for any other card tag, including one this client version does not know. The keyed `BashRow` carries the card resident below its summary row and outside that row's click target, so copying or expanding the output does not open the details panel; the render-site fallback row keeps it behind its existing expand control. Rows cap at `CHAT_TERMINAL_MAX_LINES` (8) against the panel's 16, which is what keeps a summary surface bounded — the panel stays the single-call reading surface. Inline output is licensed for this intent alone; a generic tool's content remains panel-only ([decision](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.md)).
|
||||
A tool call declaring the `terminal` render intent renders its command output inline, at both conversation render sites, through ui-primitives' `TerminalBlock`. `contract/terminal-card-model.ts` is the single derivation from the snapshot's `callView`/`resultView` pair, so the sites cannot disagree about a command, its cwd, or its exit status; it yields null — the generic path — for any other card tag, including one this client version does not know. Both sites therefore also show the card's run-state dot, which is the same `StateDot` semantic a tool row's leading icon carries, so a row and its own card always agree about one command's state. The keyed `BashRow` carries the card resident below its summary row and outside that row's click target, so copying or expanding the output does not open the details panel; the render-site fallback row keeps it behind its existing expand control. Rows cap at `CHAT_TERMINAL_MAX_LINES` (8) against the panel's 16, which is what keeps a summary surface bounded — the panel stays the single-call reading surface. Inline output is licensed for this intent alone; a generic tool's content remains panel-only ([decision](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.md)).
|
||||
|
||||
Tool rows are slots too — the standalone tool ring (`ToolViewRegistry`/`ctx.toolviews`/outlet) is retired. The chat entry declares the keyed `'conversation.chat.toolview'` hole (session scope; the key space is runtime-open); its render site dispatches per row via `entryKey: toolName` with `GenericToolCard` as the call-site `fallback`. The owner payload is the uniform `ToolRowOwnerProps` (`callId`/`toolName`/`block`/`openDetails`) and `ToolRowProps` pre-composes it with the session standard kit. A registrant is a plain plugin: `ctx.slots.register({ name: 'conversation.chat.toolview', key: '<tool>', inject? }, Row)` with `inject: ['slots', 'conversation']` as the load-order seam (apply mounts ConversationService after the chat registration, so the service being present guarantees the slot is declared); session differentiation happens inside the component (`useSessions` reading `parentId` — the bash sample is the third-party-posture exemplar). Trajectory/waterfall toolview slots share this shape and land with their own render sites (RendersCheck rejects a declaration nobody renders).
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
通用工具行把内置的 bash、read、search、write、edit 和 run_code 名称归入专用视觉变体。文件系统变体会渲染 edit 图标和 `Write · <path>` 或 `Edit · <path>` 摘要,同时保留共享的行到详情交互。code 变体以模型撰写的 `description` 作摘要,展开后显示程序本身;其已记录的子调用经由同一个键控 toolview 空位渲染为始终可见的嵌套行(自定义注册和 GenericToolCard fallback 原样适用于子行),details 面板则会根据选中的子调用 id 解析出其完整记录的参数与完整输出。Cordis 生命周期工具复用这些通用变体,同时以统一的 Cordis 强调色呈现 `Inspect`、`Mount temporary Plugin` 和 `Unmount temporary Plugin`;mount 行保留 code 变体的可展开源码渲染。
|
||||
|
||||
声明 `terminal` 渲染意图的工具调用,会在两个对话渲染点上都通过 ui-primitives 的 `TerminalBlock` 内联渲染其命令输出。`contract/terminal-card-model.ts` 是从快照的 `callView`/`resultView` 对推导的唯一位置,因此两个渲染点不可能在命令、cwd 或退出状态上产生分歧;对任何其他 card 标签——包括当前客户端版本不认识的标签——它返回 null,落回通用路径。键控的 `BashRow` 把卡片常驻在摘要行下方、且位于该行点击目标之外,因此复制或展开输出不会打开详情面板;渲染点兜底行则保持其既有的展开控件。行的上限是 `CHAT_TERMINAL_MAX_LINES`(8),面板为 16,正是这一点让摘要面保持有界——面板仍是单次调用的阅读面。内联输出只对该意图开放;通用工具的内容仍然只在面板中呈现([决策](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.md))。
|
||||
声明 `terminal` 渲染意图的工具调用,会在两个对话渲染点上都通过 ui-primitives 的 `TerminalBlock` 内联渲染其命令输出。`contract/terminal-card-model.ts` 是从快照的 `callView`/`resultView` 对推导的唯一位置,因此两个渲染点不可能在命令、cwd 或退出状态上产生分歧;对任何其他 card 标签——包括当前客户端版本不认识的标签——它返回 null,落回通用路径。因此两个渲染点也都显示卡片的运行状态点,它与工具行行首图标承载同一套 `StateDot` 语义,所以一行与其自身的卡片对同一条命令的状态总是一致。键控的 `BashRow` 把卡片常驻在摘要行下方、且位于该行点击目标之外,因此复制或展开输出不会打开详情面板;渲染点兜底行则保持其既有的展开控件。行的上限是 `CHAT_TERMINAL_MAX_LINES`(8),面板为 16,正是这一点让摘要面保持有界——面板仍是单次调用的阅读面。内联输出只对该意图开放;通用工具的内容仍然只在面板中呈现([决策](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.md))。
|
||||
|
||||
工具行同样是 slot:独立工具环(`ToolViewRegistry`/`ctx.toolviews`/outlet)已经退役。聊天配置项声明键控的 `'conversation.chat.toolview'` 空位(Session scope;key 空间在运行时开放);其渲染点逐行通过 `entryKey: toolName` 分发,并以 `GenericToolCard` 作为调用点 `fallback`。owner 载荷是统一的 `ToolRowOwnerProps`(`callId`/`toolName`/`block`/`openDetails`),`ToolRowProps` 则预先将其与 Session 标准工具包组合。注册方只是普通插件:`ctx.slots.register({ name: 'conversation.chat.toolview', key: '<tool>', inject? }, Row)`,以 `inject: ['slots', 'conversation']` 作为加载顺序 seam(apply 在聊天注册后挂载 ConversationService,因此服务存在即可保证 slot 已声明);Session 区分在组件内部完成(`useSessions` 读取 `parentId`,bash 示例是第三方姿态的范例)。Trajectory/waterfall 工具视图 slot 共享此形状,并随各自的渲染点落地(RendersCheck 会拒绝没有任何渲染方的声明)。
|
||||
|
||||
|
||||
@@ -28,6 +28,11 @@ afterEach(cleanup)
|
||||
*/
|
||||
const RAW = { normalizer: (text: string) => text }
|
||||
|
||||
/** The rendered card's run-state dot state, so a render site cannot silently drop it. */
|
||||
function runStateOf(container: HTMLElement): string | null {
|
||||
return container.querySelector('[data-terminal] [data-state]')?.getAttribute('data-state') ?? null
|
||||
}
|
||||
|
||||
const SID = 's1' as SessionId
|
||||
|
||||
const ARGS = '{"command":"ls -la","description":"List files"}'
|
||||
@@ -137,6 +142,9 @@ describe('chat row terminal body', () => {
|
||||
fireEvent.click(view.container.querySelector('button')!)
|
||||
expect(view.getByText('ls -la')).toBeTruthy()
|
||||
expect(view.queryByText('复制')).toBeNull()
|
||||
// The card states its own run state: a running command reads as running
|
||||
// even though it has no output yet to distinguish it from an empty settle.
|
||||
expect(runStateOf(view.container)).toBe('ongoing')
|
||||
})
|
||||
|
||||
it('a non-terminal call keeps the args-JSON text body', () => {
|
||||
@@ -183,6 +191,19 @@ describe('BashRow terminal card', () => {
|
||||
expect(openDetails).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
// The row's leading StateDot and the card's run-state dot describe the same
|
||||
// command, so a running row whose card claimed 'done' would be a contradiction
|
||||
// the reader sees on one line.
|
||||
it('agrees with the summary row about the run state', () => {
|
||||
const runningView = render(<BashRow {...rowProps(running())} />)
|
||||
expect(runningView.container.querySelector('[data-variant="bash"]')?.getAttribute('data-state')).toBe('running')
|
||||
expect(runStateOf(runningView.container)).toBe('ongoing')
|
||||
cleanup()
|
||||
const settledView = render(<BashRow {...rowProps(settled())} />)
|
||||
expect(settledView.container.querySelector('[data-variant="bash"]')?.getAttribute('data-state')).toBe('ok')
|
||||
expect(runStateOf(settledView.container)).toBe('done')
|
||||
})
|
||||
|
||||
it('a non-terminal bash call (background start) renders the summary row alone', () => {
|
||||
const view = render(<BashRow {...rowProps(settled({
|
||||
callView: { card: 'generic', title: 'sleep 30', kind: 'execute' },
|
||||
@@ -246,6 +267,7 @@ describe('DetailsPanel Output section', () => {
|
||||
const view = mount(snapshot({ runningCalls: [running()] }), target)
|
||||
expect(view.getByText('ls -la')).toBeTruthy()
|
||||
expect(view.queryByText('运行中…')).toBeNull()
|
||||
expect(runStateOf(view.container)).toBe('ongoing')
|
||||
})
|
||||
|
||||
it('a running non-terminal call keeps the 运行中… placeholder', () => {
|
||||
|
||||
@@ -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 packages/client/ui-primitives/README.md
|
||||
README.md: c9c70f29804ac4e6783486595460bf07499e1dff
|
||||
README.zh.md: 254fc5ba5aef553fd447338353a0c5311ddbd98a
|
||||
README.md: b0387debbecb713b1e4af2b1497e81ddda083a51
|
||||
README.zh.md: d04a34951422c9cdd02421759a4e29672b1f7a54
|
||||
|
||||
@@ -10,7 +10,7 @@ Pure React atoms (zero cordis): StateDot, ic_ds_* icons, Button/Pill/Menu/Modal/
|
||||
|
||||
## Terminal output
|
||||
|
||||
`TerminalBlock` renders a shell command as a terminal surface: a prompt line (shortened `cwd` label plus the command), the command's output, a status pill for a non-zero exit code or a terminating signal, and a copy control that writes the raw `output` prop. ANSI escape sequences are parsed with the `anser` runtime dependency into React spans; basic-16 foreground colors map onto `--dsw-*` tokens, while 256-palette and truecolor values pass through as literal rgb. Output keeps `white-space: pre` with horizontal scrolling, so column-aligned output holds its alignment instead of soft-wrapping, and collapses to a head slice plus a tail slice past `maxLines` (default 16, the TUI transcript's split arithmetic) behind an expand button. Rationale: [the web terminal card note](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.md).
|
||||
`TerminalBlock` renders a shell command as a terminal surface: a prompt line (a run-state `StateDot` ahead of the shortened `cwd` label, then the command), the command's output, a status pill for a non-zero exit code or a terminating signal, and a copy control that writes the raw `output` prop. The dot reaches three of `StateDot`'s states — the spinning ring while `running`, red for the same exit status that renders the pill, green otherwise — so a card states whether its command is still running rather than leaving that to be inferred from the presence of output; it carries a visually hidden text label because `StateDot` is `aria-hidden`. ANSI escape sequences are parsed with the `anser` runtime dependency into React spans; basic-16 foreground colors map onto `--dsw-*` tokens, while 256-palette and truecolor values pass through as literal rgb. Output keeps `white-space: pre` with horizontal scrolling, so column-aligned output holds its alignment instead of soft-wrapping, and collapses to a head slice plus a tail slice past `maxLines` (default 16, the TUI transcript's split arithmetic) behind an expand button. Rationale: [the web terminal card note](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.md).
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
## 终端输出
|
||||
|
||||
`TerminalBlock` 将一条 shell 命令渲染为终端表层:提示行(缩短后的 `cwd` 标签加命令)、命令输出、非零退出码或终止信号对应的状态胶囊,以及写入原始 `output` prop 的复制控件。ANSI 转义序列通过运行时依赖 `anser` 解析为 React span;基础 16 色前景色映射到 `--dsw-*` token,而 256 色板与真彩色值按字面 rgb 透传。输出保持 `white-space: pre` 并支持横向滚动,因此按列对齐的输出保留其对齐而不会软换行;超过 `maxLines`(默认 16,与 TUI 转录相同的切分算法)时折叠为头部切片加尾部切片,由展开按钮控制。原理:[Web 终端卡片笔记](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.md)。
|
||||
`TerminalBlock` 将一条 shell 命令渲染为终端表层:提示行(缩短后的 `cwd` 标签之前是一枚运行状态 `StateDot`,其后是命令)、命令输出、非零退出码或终止信号对应的状态胶囊,以及写入原始 `output` prop 的复制控件。该状态点用到 `StateDot` 的三种状态——`running` 期间为旋转圆环,与渲染状态胶囊相同的退出状态为红色,其余为绿色——因此卡片直接陈述其命令是否仍在运行,而不是让人从有无输出中推断;由于 `StateDot` 是 `aria-hidden`,它同时携带一处视觉隐藏的文本标签。ANSI 转义序列通过运行时依赖 `anser` 解析为 React span;基础 16 色前景色映射到 `--dsw-*` token,而 256 色板与真彩色值按字面 rgb 透传。输出保持 `white-space: pre` 并支持横向滚动,因此按列对齐的输出保留其对齐而不会软换行;超过 `maxLines`(默认 16,与 TUI 转录相同的切分算法)时折叠为头部切片加尾部切片,由展开按钮控制。原理:[Web 终端卡片笔记](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.md)。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ export type StateDotState = 'done' | 'warning' | 'ongoing' | 'error'
|
||||
*/
|
||||
export function StateDot({ state, size = 10, className }: {
|
||||
state: StateDotState
|
||||
size?: number
|
||||
className?: string
|
||||
size?: number | undefined
|
||||
className?: string | undefined
|
||||
}) {
|
||||
const gradientId = useId()
|
||||
if (state === 'ongoing') {
|
||||
|
||||
@@ -36,6 +36,23 @@
|
||||
font: var(--dsw-font-markdown-code-block);
|
||||
}
|
||||
|
||||
/* The dot sits on the prompt row's baseline box, which is a code-font line, so
|
||||
it is centered against that line's box rather than sitting on the baseline. */
|
||||
.runState {
|
||||
flex: none;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* The dot is aria-hidden; this is its text label for assistive technology. */
|
||||
.runStateLabel {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cwd {
|
||||
flex: none;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// TerminalBlock: the terminal surface for a shell command and its output —
|
||||
// prompt line (shortened cwd + command), ANSI-colored output, settled exit
|
||||
// status, and a copy control for the raw output. Output never soft-wraps:
|
||||
// prompt line (run-state dot + shortened cwd + command), ANSI-colored output,
|
||||
// settled exit status, and a copy control for the raw output. Output never soft-wraps:
|
||||
// column-aligned output (ls, tables, box drawing) keeps its alignment and
|
||||
// scrolls horizontally instead of folding. Colors resolve through --dsw-*
|
||||
// tokens; ANSI parsing lives in ansi.ts.
|
||||
@@ -10,6 +10,7 @@ import clsx from 'clsx'
|
||||
import { parseAnsiLines, type AnsiLine } from './ansi.ts'
|
||||
import { writeClipboard } from './clipboard.ts'
|
||||
import { Pill } from './Pill.tsx'
|
||||
import { StateDot, type StateDotState } from './StateDot.tsx'
|
||||
import css from './TerminalBlock.module.css'
|
||||
|
||||
/**
|
||||
@@ -70,6 +71,31 @@ function statusText(exitCode: number | undefined, signal: string | undefined): s
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Run-state indicator for the command, shown at the head of the prompt line so
|
||||
* the card states whether the command is still running without the reader
|
||||
* having to infer it from the presence of output. Three of {@link StateDotState}'s
|
||||
* four states are reachable: the spinning ring while running (the same
|
||||
* indicator a running tool row's leading icon uses, so the row and its card
|
||||
* never disagree), green for a clean settle, red for a signal or a non-zero
|
||||
* exit — the same status distinction {@link statusText} draws for the pill. A
|
||||
* settled command whose exit status never reached the view counts as a clean
|
||||
* settle: the view says it finished and says nothing went wrong.
|
||||
* @param running - the command has not settled.
|
||||
* @param exitCode - settled exit code, when known.
|
||||
* @param signal - settled terminating signal name, when known.
|
||||
* @returns the dot's state and its text label, since the dot is aria-hidden.
|
||||
*/
|
||||
function runState(
|
||||
running: boolean,
|
||||
exitCode: number | undefined,
|
||||
signal: string | undefined,
|
||||
): { state: StateDotState; label: string } {
|
||||
if (running) return { state: 'ongoing', label: '运行中' }
|
||||
if (statusText(exitCode, signal) !== undefined) return { state: 'error', label: '失败' }
|
||||
return { state: 'done', label: '已完成' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Render one parsed output line. Runs without SGR state render as bare text,
|
||||
* so uncolored output carries no span wrappers.
|
||||
@@ -120,6 +146,7 @@ export function TerminalBlock({
|
||||
const onToggle = useCallback(() => { setExpanded(value => !value) }, [])
|
||||
|
||||
const status = statusText(exitCode, signal)
|
||||
const state = runState(running, exitCode, signal)
|
||||
const empty = text.trim() === ''
|
||||
const hidden = lines.length - maxLines
|
||||
const capped = hidden > 0 && !expanded
|
||||
@@ -132,6 +159,8 @@ export function TerminalBlock({
|
||||
<div className={clsx(css.block, className)} data-terminal="" data-running={running ? '' : undefined}>
|
||||
<div className={css.header}>
|
||||
<div className={css.prompt}>
|
||||
<StateDot state={state.state} className={css.runState} />
|
||||
<span className={css.runStateLabel}>{state.label}</span>
|
||||
<span className={css.cwd}>{cwd === undefined ? '$' : promptLabel(cwd, home)}</span>
|
||||
<span className={css.command}>{command}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment jsdom
|
||||
// TerminalBlock: the prompt label's cwd shortening, the running/empty/settled
|
||||
// arms, the exit-status pill, the head/tail height cap and its expand control,
|
||||
// arms, the prompt line's run-state dot, the exit-status pill, the head/tail height cap and its expand control,
|
||||
// and the copy control writing the raw output on both the accepted and the
|
||||
// refused clipboard paths. writeClipboard's own return contract is pinned here
|
||||
// too, since it is the seam both copy controls in this package share; the
|
||||
@@ -25,6 +25,15 @@ function outputLines(container: HTMLElement): string[] {
|
||||
return [...container.querySelectorAll('[class^="_line_"]')].map(row => row.textContent ?? '')
|
||||
}
|
||||
|
||||
/** The prompt line's run-state dot: its StateDot state plus the hidden text label beside it. */
|
||||
function runStateOf(container: HTMLElement): { state: string | null; label: string | undefined } {
|
||||
const dot = container.querySelector('[class*="_runState_"][data-state]')
|
||||
return {
|
||||
state: dot?.getAttribute('data-state') ?? null,
|
||||
label: container.querySelector('[class^="_runStateLabel_"]')?.textContent ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/** `count` numbered output lines, without the terminating newline. */
|
||||
function body(count: number): string {
|
||||
return Array.from({ length: count }, (_value, index) => `line ${index + 1}`).join('\n')
|
||||
@@ -128,7 +137,8 @@ describe('TerminalBlock states', () => {
|
||||
|
||||
it('renders ANSI runs as styled spans and plain text bare', () => {
|
||||
const view = render(<TerminalBlock command="ls" output={`${ESC}[31mbad${ESC}[39m ok`} />)
|
||||
const span = view.container.querySelector('span[style]')
|
||||
// Scoped to a line: the prompt line's run-state dot is a styled span too.
|
||||
const span = view.container.querySelector('[class^="_line_"] span[style]')
|
||||
expect(span?.textContent).toBe('bad')
|
||||
expect(span?.getAttribute('style')).toContain('--dsw-alias-state-error-primary')
|
||||
expect(outputLines(view.container)).toEqual(['bad ok'])
|
||||
@@ -163,6 +173,46 @@ describe('TerminalBlock status pill', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('TerminalBlock run-state dot', () => {
|
||||
it('shows the spinning ring and its running label while the command runs', () => {
|
||||
const view = render(<TerminalBlock command="sleep 5" running />)
|
||||
expect(runStateOf(view.container)).toEqual({ state: 'ongoing', label: '运行中' })
|
||||
})
|
||||
|
||||
it('shows the done dot for a clean settled exit', () => {
|
||||
const view = render(<TerminalBlock command="true" output="a" exitCode={0} />)
|
||||
expect(runStateOf(view.container)).toEqual({ state: 'done', label: '已完成' })
|
||||
})
|
||||
|
||||
it('counts a settled command with no exit status as a clean settle', () => {
|
||||
const view = render(<TerminalBlock command="ls" output="a" />)
|
||||
expect(runStateOf(view.container)).toEqual({ state: 'done', label: '已完成' })
|
||||
})
|
||||
|
||||
it('shows the error dot for a non-zero exit', () => {
|
||||
const view = render(<TerminalBlock command="false" output="a" exitCode={1} />)
|
||||
expect(runStateOf(view.container)).toEqual({ state: 'error', label: '失败' })
|
||||
})
|
||||
|
||||
it('shows the error dot for a signal, whatever the exit code says', () => {
|
||||
const view = render(<TerminalBlock command="sleep 9" output="a" exitCode={0} signal="SIGKILL" />)
|
||||
expect(runStateOf(view.container)).toEqual({ state: 'error', label: '失败' })
|
||||
})
|
||||
|
||||
// The dot precedes the prompt label, which is what makes it read as the
|
||||
// state OF this command rather than of the card's chrome.
|
||||
it('places the dot ahead of the prompt label and the command', () => {
|
||||
const view = render(<TerminalBlock command="ls" cwd="/srv/app" output="a" />)
|
||||
const prompt = view.container.querySelector('[class^="_prompt_"]')
|
||||
expect([...prompt!.children].map(node => node.textContent)).toEqual(['', '已完成', 'app', 'ls'])
|
||||
})
|
||||
|
||||
it('keeps the running dot even while a settled-looking status pill is supplied', () => {
|
||||
const view = render(<TerminalBlock command="sleep 5" running signal="SIGINT" />)
|
||||
expect(runStateOf(view.container)).toEqual({ state: 'ongoing', label: '运行中' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('TerminalBlock height cap', () => {
|
||||
it('renders every line and no expand control under the cap', () => {
|
||||
const view = render(<TerminalBlock command="ls" output={body(4)} maxLines={4} />)
|
||||
|
||||
Reference in New Issue
Block a user