Merge pull request #6264 from deepseek-harness/fix/tool-exit-badge
fix(ui-tool): show command exits in neutral inline capsules
This commit is contained in:
commit
e44061c934
12 files changed
+201
-33
No files matched your search
@@ -78,6 +78,8 @@ interface GalleryCall {
|
||||
/** The five successful writes; docs/press.md and src/tokens.css join via other tools. */
|
||||
const WRITES = ['site/report.html', 'a/style.css', 'b/style.css', 'site/index.html', 'site/app.js']
|
||||
|
||||
const LONG_COMMAND_DESCRIPTION = 'Check every generated stylesheet and component theme against the shared design tokens, inspect the application pages for inconsistent colors, and report all remaining lint failures before publishing the production build'
|
||||
|
||||
/** The gallery turn's tool calls, in surface order. */
|
||||
const CALLS: GalleryCall[] = [
|
||||
...WRITES.map(path => ({
|
||||
@@ -166,6 +168,16 @@ const CALLS: GalleryCall[] = [
|
||||
args: { command: 'pnpm run lint', description: 'Run the lint gate', workdir: 'site' },
|
||||
result: 'style.css: unexpected hex literal\n[exit code: 1]',
|
||||
},
|
||||
{
|
||||
name: 'bash',
|
||||
args: { command: 'pnpm run lint:styles', description: LONG_COMMAND_DESCRIPTION, workdir: 'site' },
|
||||
result: 'theme.css: unexpected hex literal\n[exit code: 1]',
|
||||
},
|
||||
{
|
||||
name: 'bash',
|
||||
args: { command: 'pnpm run build', description: 'Build the site', workdir: 'site' },
|
||||
result: '(no output)\n[timed out after 100ms]\n[exit code: null]',
|
||||
},
|
||||
{
|
||||
name: 'web_search',
|
||||
args: { queries: ['clickable link styles', 'produced files ui'] },
|
||||
@@ -378,6 +390,55 @@ describe('web e2e: clickable links gallery', () => {
|
||||
expect(await fetchSummaryLink.getAttribute('target')).toBe('_blank')
|
||||
|
||||
await expandTurnProcesses(page)
|
||||
const failedCommands = [
|
||||
{ description: 'Run the lint gate', long: false },
|
||||
{ description: LONG_COMMAND_DESCRIPTION, long: true },
|
||||
].map(({ description, long }) => ({
|
||||
row: page.locator('[data-sample="bash"]').filter({ hasText: description }),
|
||||
long,
|
||||
}))
|
||||
try {
|
||||
for (const colorScheme of ['light', 'dark'] as const) {
|
||||
await page.emulateMedia({ colorScheme })
|
||||
await expect.poll(() => page.locator('body').evaluate(element => element.hasAttribute('data-ds-dark-theme'))).toBe(colorScheme === 'dark')
|
||||
for (const { row, long } of failedCommands) {
|
||||
const exitBadge = row.getByText('exit code 1', { exact: true })
|
||||
await exitBadge.waitFor()
|
||||
for (const width of ['760px', '260px']) {
|
||||
await row.evaluate((element, value) => { element.style.width = value }, width)
|
||||
const geometry = await exitBadge.evaluate((badge) => {
|
||||
const description = badge.previousElementSibling!
|
||||
const row = badge.closest('[data-sample]')!
|
||||
const textRect = description.getBoundingClientRect()
|
||||
const badgeRect = badge.getBoundingClientRect()
|
||||
const rowRect = row.getBoundingClientRect()
|
||||
return {
|
||||
neutral: getComputedStyle(description).color === getComputedStyle(row).color,
|
||||
fits: badgeRect.right <= rowRect.right + 1 && badgeRect.bottom <= rowRect.bottom + 1,
|
||||
inline: Math.abs(badgeRect.top - textRect.top) < 5,
|
||||
truncated: description.scrollWidth > description.clientWidth,
|
||||
ellipsis: getComputedStyle(description).textOverflow === 'ellipsis',
|
||||
singleLine: Math.abs(rowRect.height - textRect.height) < 1,
|
||||
height: rowRect.height,
|
||||
gap: badgeRect.left - textRect.right,
|
||||
}
|
||||
})
|
||||
const context = JSON.stringify({ width, colorScheme, long, ...geometry })
|
||||
expect(geometry.neutral, context).toBe(true)
|
||||
expect(geometry.fits, context).toBe(true)
|
||||
expect(geometry.inline, context).toBe(true)
|
||||
expect(geometry.singleLine, context).toBe(true)
|
||||
expect(geometry.height, context).toBeCloseTo(24, 0)
|
||||
if (long || width === '760px') expect(geometry.truncated, context).toBe(long)
|
||||
expect(geometry.ellipsis, context).toBe(true)
|
||||
expect(geometry.gap, context).toBeCloseTo(8, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
for (const { row } of failedCommands) await row.evaluate((element) => { element.style.width = '' })
|
||||
await page.emulateMedia({ colorScheme: null })
|
||||
}
|
||||
// Clicking the collapsed summary link opens the page without expanding the row.
|
||||
const fetchPopupPromise = page.waitForEvent('popup')
|
||||
await fetchSummaryLink.click()
|
||||
@@ -397,12 +458,20 @@ describe('web e2e: clickable links gallery', () => {
|
||||
/^Edit src\/tokens\.css/,
|
||||
/^Grep linkColor/,
|
||||
/Run the lint gate|pnpm run lint/,
|
||||
/Build the site/,
|
||||
]) {
|
||||
const toggle = page.getByRole('button', { name: row }).first()
|
||||
await toggle.waitFor({ timeout: 10_000 })
|
||||
const box = await toggle.boundingBox()
|
||||
await toggle.click(box === null ? {} : { position: { x: box.width - 8, y: box.height / 2 } })
|
||||
}
|
||||
const noExitCommand = page.locator('[data-sample="bash"]').filter({ hasText: 'Build the site' })
|
||||
expect(await noExitCommand.getAttribute('data-state')).toBe('error')
|
||||
expect(await noExitCommand.getByText('no exit code', { exact: true }).count()).toBe(1)
|
||||
const noExitTerminal = page.locator('[data-terminal]').filter({ hasText: 'pnpm run build' })
|
||||
expect(await noExitTerminal.getByText('no exit code', { exact: true }).count()).toBe(1)
|
||||
expect(await noExitTerminal.getByText('[timed out after 100ms]', { exact: true }).count()).toBe(1)
|
||||
expect(await noExitTerminal.getByText('[exit code: null]', { exact: true }).count()).toBe(0)
|
||||
// Both generic rows (the unclassified str_replace_editor and the unknown
|
||||
// design_tokens_sync) expand to their IN/OUT surfaces.
|
||||
const genericRows = page.getByRole('button', { name: /^Tool call/ })
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
- button "Copy"
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [expanded]
|
||||
- button "Wrote files, called tools, searched code, etc." [expanded]
|
||||
- button "Wrote files, ran commands, called tools, etc." [expanded]
|
||||
- button "Write site/report.html 1KB +1 -0":
|
||||
- text: Write
|
||||
- button "site/report.html"
|
||||
@@ -65,11 +65,17 @@
|
||||
- button "Inspect"
|
||||
- button "Glob **/*.css"
|
||||
- button "Bash List the built site"
|
||||
- button "Failed Bash Run the lint gate" [expanded]
|
||||
- button "Failed Bash Run the lint gate exit code 1" [expanded]
|
||||
- text: Failed site pnpm run lint exit code 1
|
||||
- button "Copy"
|
||||
- text: "style.css: unexpected hex literal"
|
||||
- button "Inspect"
|
||||
- button "Failed Bash Check every generated stylesheet and component theme against the shared design tokens, inspect the application pages for inconsistent colors, and report all remaining lint failures before publishing the production build exit code 1"
|
||||
- button "Failed Bash Build the site no exit code" [expanded]
|
||||
- text: Failed site pnpm run build no exit code
|
||||
- button "Copy"
|
||||
- text: (no output) [timed out after {{duration}}]
|
||||
- button "Inspect"
|
||||
- button "Search clickable link styles, produced files ui" [expanded]
|
||||
- paragraph:
|
||||
- text: Unify links per
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: d152484eb41ac6b4
|
||||
zh: 09388d293f9be9cb
|
||||
/deepseek-ai-dsh-client-ui-tool/use-this-package:
|
||||
en: 3b9b719ca4e4cc62
|
||||
zh: a14b63a16476d684
|
||||
en: c6d4e666191521b2
|
||||
zh: 2498020e7080e172
|
||||
/deepseek-ai-dsh-client-ui-tool/use-this-package/registering-a-business-tool-view:
|
||||
en: 2e3c306b13d3bc25
|
||||
zh: 77127f01763b3976
|
||||
|
||||
@@ -27,7 +27,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
Tool calls appear in the conversation as cards: a root call tree with its nested subcalls, each atomic call rendered by its owning view. Every lifecycle state retains the tool's ordinary business glyph; failure and interruption remain explicit through the frozen call/result state, accessible status text, and failure summary. Users can open files or inspect calls through the Host callbacks. A collapsed `web_fetch` row links its http(s) URL, which opens in a new browser tab.
|
||||
|
||||
Shared Tool rows and Bash rows retain error and warning colors for failed and stopped summaries, including on hover. Hover darkens only summaries without those states.
|
||||
Settled foreground commands with a nonzero exit code, a terminating signal, or an explicit missing exit code keep their description in the ordinary gray text and show a neutral capsule with a red dot immediately after it. The capsule displays only the recorded code, signal, or localized missing-code status and stays on the same line; the description truncates with an ellipsis to reserve its space. Running, successful, and background calls omit it. Generic execution errors and stopped summaries retain their error and warning colors, including on hover.
|
||||
|
||||
Before dispatch, a named model call appears as one non-expandable row with its tool-owned icon and title. A generic row shows `Tool call · <tool name>`. Read/write/edit show an openable path once `file_path` closes and decodes without errors; write/edit show `NKB` after that path once content arrives, retaining it during execution and after success, before any diff totals. N rounds the decoded input's UTF-16 length up in units of 1024, summing old and new text for edit; it is not file bytes. Error and stopped rows omit the size and diff totals. Command rows show the streamed description. `tool/call` enables the existing call presentation; completing an argument block alone does not start execution.
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ kind: "package-reference"
|
||||
|
||||
工具调用在对话中显示为卡片:一个根调用树带其嵌套子调用,每个原子调用由所属视图渲染。所有生命周期状态都保留工具的普通业务图标;失败与中断仍通过冻结调用/结果状态、无障碍状态文本和失败摘要明确表达。用户可通过宿主回调打开文件或检查调用。折叠的 `web_fetch` 行把其 http(s) URL 显示为链接,在新浏览器标签页中打开。
|
||||
|
||||
共享工具行和 Bash 行的失败、停止摘要在悬停时仍保留错误色和警告色;只有不处于这两种状态的摘要会在悬停时加深。
|
||||
已结束的前台命令若返回非零退出码、终止信号,或明确记录为无退出码,描述保持普通灰色,后面紧接带红点的中性胶囊。胶囊仅显示记录的退出码、信号或本地化的“未正常退出”状态,并保持在同一行;描述超出可用宽度时显示省略号,为胶囊预留空间。运行中、成功和后台调用不显示胶囊。通用执行错误与停止摘要仍保留错误色和警告色,悬停时也不改变。
|
||||
|
||||
派发前,模型已给出名称的调用显示为不可展开的一行,使用工具自己的图标与标题。通用行显示为 `工具调用 · <工具名>`。read/write/edit 在 `file_path` 闭合且解码无误后显示可打开的路径;write/edit 从内容到达起在该路径后显示 `NKB`,执行中和成功后仍保留,位于差异统计之前。N 按解码后输入的 UTF-16 长度除以 1024 向上取整,edit 合计新旧文本长度;它不是文件字节数。失败和停止的行省略大小与差异统计。命令行显示流入中的描述。`tool/call` 才启用既有调用展示;参数块结束本身不代表开始执行。
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
.badge {
|
||||
flex: none;
|
||||
margin-left: 8px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
flex: none;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
corner-shape: round;
|
||||
background: var(--dsw-alias-state-error-primary);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/** Inline exit status for a settled command. */
|
||||
import { Tag } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import css from './ExitCodeBadge.module.css'
|
||||
|
||||
/**
|
||||
* Render a neutral capsule with a failure dot and a localized exit status.
|
||||
* @param props - localized exit code or terminating signal label.
|
||||
* @returns the exit status capsule.
|
||||
*/
|
||||
export function ExitCodeBadge({ label }: { label: string }) {
|
||||
return (
|
||||
<Tag tone="neutral" className={css.badge}>
|
||||
<span className={css.dot} aria-hidden />
|
||||
{label}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { CHAT_READ_MAX_LINES, type ReadCardModel } from '../models/read-card-mod
|
||||
import type { ImageCardModel } from '../models/image-card-model.ts'
|
||||
import { CHAT_SEARCH_MAX_LINES, type SearchCardModel } from '../models/search-card-model.ts'
|
||||
import {
|
||||
localizeTerminalCardModel, terminalBlockLabels, type TerminalCardModel,
|
||||
localizeTerminalCardModel, terminalBlockLabels, terminalExitLabel, type TerminalCardModel,
|
||||
} from '../models/terminal-card-model.ts'
|
||||
import {
|
||||
codeToolbarLabels, diffBlockLabels, readBlockLabels, searchBlockLabels, webBlockLabels,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import type { WebCardModelProps } from '../models/web-card-model.ts'
|
||||
import { AskQuestionCard } from './AskQuestionCard.tsx'
|
||||
import { ToolDetails, type ToolDetailsModel } from './ToolDetails.tsx'
|
||||
import { ExitCodeBadge } from './ExitCodeBadge.tsx'
|
||||
import css from './ToolRow.module.css'
|
||||
|
||||
export interface ToolRowProps {
|
||||
@@ -50,7 +51,10 @@ export interface ToolRowProps {
|
||||
output?: string | null | undefined
|
||||
/** Ask-user transcript card; card fields are mutually exclusive and replace text sections. */
|
||||
askQuestion?: AskQuestionCardModel | null | undefined
|
||||
/** Error first line shown as the collapsed summary on an error row; null/absent = keep `summary`. */
|
||||
/**
|
||||
* First error line for rows without an exit status capsule; null/absent keeps
|
||||
* the ordinary summary. Rows with an exit capsule retain the command description.
|
||||
*/
|
||||
errorSummary?: string | null | undefined
|
||||
/** Terminal card; card fields are mutually exclusive and replace text sections. */
|
||||
terminal?: TerminalCardModel | null | undefined
|
||||
@@ -176,10 +180,9 @@ export const ToolRow = memo(function ToolRow({
|
||||
const status = stateStatus(state, t)
|
||||
const running = state === 'running' || state === 'preparing'
|
||||
const normalSummary = terminalBody?.description ?? (open ? detailsBody?.expandedSummary ?? summary : summary)
|
||||
// A failure keeps its first result line when available and otherwise turns
|
||||
// the ordinary summary red. An interruption turns the tool-owned summary
|
||||
// amber while retaining the business icon and hidden state announcement.
|
||||
const failureLine = state === 'error' ? errorSummary ?? normalSummary : null
|
||||
const exitLabel = terminalExitLabel(terminal, t)
|
||||
// Generic failures retain their first result line; command exits keep their description.
|
||||
const failureLine = state === 'error' && exitLabel === null ? errorSummary ?? normalSummary : null
|
||||
const summaryText = failureLine ?? normalSummary
|
||||
// Diff totals remain visible after the optional summary suffix.
|
||||
const diffStat = useMemo(() => diffBody === null ? null : diffTotals(diffBody.card.diffs), [diffBody])
|
||||
@@ -210,7 +213,7 @@ export const ToolRow = memo(function ToolRow({
|
||||
its title shows no trailing dot). */
|
||||
<>
|
||||
<span className={css.sep} data-shimmer-decoration aria-hidden />
|
||||
{openFile !== undefined ? (
|
||||
{exitLabel === null && openFile !== undefined ? (
|
||||
<button
|
||||
type="button"
|
||||
className={css.fileLink}
|
||||
@@ -219,7 +222,7 @@ export const ToolRow = memo(function ToolRow({
|
||||
>
|
||||
<TextShimmer>{summaryText}</TextShimmer>
|
||||
</button>
|
||||
) : linkHref !== undefined ? (
|
||||
) : exitLabel === null && linkHref !== undefined ? (
|
||||
<a
|
||||
className={css.fileLink}
|
||||
href={linkHref}
|
||||
@@ -234,13 +237,14 @@ export const ToolRow = memo(function ToolRow({
|
||||
<span
|
||||
className={clsx(
|
||||
css.summary,
|
||||
state === 'error' && css.errorSummary,
|
||||
state === 'stopped' && css.stoppedSummary,
|
||||
exitLabel === null && state === 'error' && css.errorSummary,
|
||||
exitLabel === null && state === 'stopped' && css.stoppedSummary,
|
||||
)}
|
||||
>
|
||||
<TextShimmer>{summaryText}</TextShimmer>
|
||||
</span>
|
||||
)}
|
||||
{exitLabel !== null && <ExitCodeBadge label={exitLabel} />}
|
||||
{suffix !== null && (
|
||||
<TextShimmer className={css.summarySuffix}>{suffix}</TextShimmer>
|
||||
)}
|
||||
@@ -250,7 +254,7 @@ export const ToolRow = memo(function ToolRow({
|
||||
</TextShimmer>
|
||||
)}
|
||||
</>
|
||||
), [summaryLinkKeyDown, linkHref, openFile, state, suffix, totals, summaryText])
|
||||
), [summaryLinkKeyDown, linkHref, openFile, state, suffix, totals, summaryText, exitLabel])
|
||||
const expandedContent = useMemo(() => open ? (
|
||||
<div className={clsx(css.bodyWrap, detailsBody !== null && css.detailsBodyWrap)}>
|
||||
{askQuestionBody !== null
|
||||
|
||||
@@ -86,11 +86,10 @@ export function localizeTerminalCardModel(
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a settled terminal card reports a failing exit — a non-zero code
|
||||
* or a terminating signal. The bash tool settles a failing command as a
|
||||
* completed call (`isError` stays false: the exit status is result data), so
|
||||
* this is the collapsed row's only failure signal; without it the red exit
|
||||
* pill would be visible only after expanding the card.
|
||||
* True when a settled terminal card reports a failing exit — a non-zero code,
|
||||
* an explicitly missing exit code, or a terminating signal. The bash tool settles
|
||||
* a failing command as a completed call (`isError` stays false: the exit status is result data), so
|
||||
* the row retains that failure state alongside its exit capsule.
|
||||
* @param model - a derived terminal card.
|
||||
* @returns whether the card's exit status is a failure.
|
||||
*/
|
||||
@@ -99,6 +98,20 @@ export function terminalFailed(model: TerminalCardModel): boolean {
|
||||
return running !== true && ((exitCode !== undefined && exitCode !== 0) || signal !== undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
* Localize a settled foreground command's exit status without inventing a code.
|
||||
* @param model - terminal card, absent for generic execution errors.
|
||||
* @param t - conversation locale.
|
||||
* @returns the capsule label, or null for running, background, or successful calls.
|
||||
*/
|
||||
export function terminalExitLabel(model: TerminalCardModel | null | undefined, t: TranslateNS<'conversation'>): string | null {
|
||||
if (model == null || model.card.background || !terminalFailed(model)) return null
|
||||
const { exitCode, signal } = model.card
|
||||
if (signal !== undefined) return t('terminal.signal', { signal })
|
||||
if (exitCode == null) return t('terminal.noExitCode')
|
||||
return t('terminal.exitCode', { code: exitCode })
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a shell call's workdir for display: an absolute path is used as-is,
|
||||
* a relative one joins under the session workspace, and an omitted one is the
|
||||
@@ -277,11 +290,11 @@ function terminalSendCall(name: string, args: Record<string, unknown>): Terminal
|
||||
* @param text - rendered shell result text.
|
||||
* @returns output with a trailing exit-code or signal marker extracted.
|
||||
*/
|
||||
function parseExitStatus(text: string): { output: string; exitCode?: number; signal?: string } {
|
||||
function parseExitStatus(text: string): { output: string; exitCode?: number | null; signal?: string } {
|
||||
const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text)
|
||||
if (signal?.[1] !== undefined) return { output: text.slice(0, signal.index), signal: signal[1] }
|
||||
const exit = /\n\[exit code: (\d+)\]$/.exec(text)
|
||||
if (exit?.[1] !== undefined) return { output: text.slice(0, exit.index), exitCode: Number(exit[1]) }
|
||||
const exit = /\n\[exit code: (-?\d+|null)\]$/.exec(text)
|
||||
if (exit?.[1] !== undefined) return { output: text.slice(0, exit.index), exitCode: exit[1] === 'null' ? null : Number(exit[1]) }
|
||||
return { output: text, exitCode: 0 }
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,11 @@ import {
|
||||
terminalBlockLabels,
|
||||
terminalCardModel,
|
||||
terminalFailed,
|
||||
terminalExitLabel,
|
||||
} from '../models/terminal-card-model.ts'
|
||||
import { formatToolBody, toolRowModel, type ToolRowState } from '../models/tool-call-model.ts'
|
||||
import { CONVERSATION_NS as NS } from '../../locale.ts'
|
||||
import { ExitCodeBadge } from '../components/ExitCodeBadge.tsx'
|
||||
import css from './bash-sample.module.css'
|
||||
|
||||
type BashRowProps = ToolCallViewProps & PropsLocale<'conversation'>
|
||||
@@ -49,8 +51,7 @@ export const BashRow = memo(function BashRow({ toolName, block, sessionId, useSe
|
||||
const terminalModel = useMemo(() => terminalCardModel(block, cwd), [block, cwd])
|
||||
const terminal = useMemo(() => terminalModel === null ? null : localizeTerminalCardModel(terminalModel, t), [terminalModel, t])
|
||||
const labels = useMemo(() => terminalBlockLabels(t), [t])
|
||||
// A failing exit status is the terminal card's own error signal (the call
|
||||
// itself settles isError:false), surfaced through the row's error summary.
|
||||
// Exit failures are result data even when the tool call itself succeeds.
|
||||
const state = model.state === 'ok' && terminalModel !== null && terminalFailed(terminalModel)
|
||||
? 'error'
|
||||
: model.state
|
||||
@@ -71,7 +72,8 @@ export const BashRow = memo(function BashRow({ toolName, block, sessionId, useSe
|
||||
[genericBody, model.bodyRaw, model.variant, open],
|
||||
)
|
||||
const normalSummary = terminal?.description ?? model.summary
|
||||
const settlementLine = state === 'error'
|
||||
const exitLabel = terminalExitLabel(terminalModel, t)
|
||||
const settlementLine = exitLabel !== null ? null : state === 'error'
|
||||
? model.errorSummary ?? normalSummary
|
||||
: state === 'stopped' ? t('bash.stopped') : null
|
||||
const running = state === 'running' || state === 'preparing'
|
||||
@@ -111,11 +113,12 @@ export const BashRow = memo(function BashRow({ toolName, block, sessionId, useSe
|
||||
<span className={css.sep} data-shimmer-decoration aria-hidden />
|
||||
<span className={clsx(
|
||||
css.summary,
|
||||
state === 'error' && css.errorSummary,
|
||||
state === 'stopped' && css.stoppedSummary,
|
||||
exitLabel === null && state === 'error' && css.errorSummary,
|
||||
exitLabel === null && state === 'stopped' && css.stoppedSummary,
|
||||
)}>
|
||||
<TextShimmer>{settlementLine ?? normalSummary}</TextShimmer>
|
||||
</span>
|
||||
{exitLabel !== null && <ExitCodeBadge label={exitLabel} />}
|
||||
</TextShimmer>
|
||||
</div>
|
||||
{open && (
|
||||
|
||||
@@ -12,7 +12,7 @@ import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { en as commonEn } from '@deepseek-ai/dsh-client-locale/src/locales/en.ts'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import {
|
||||
localizeTerminalCardModel, terminalCardModel, terminalFailed,
|
||||
localizeTerminalCardModel, terminalCardModel, terminalFailed, terminalExitLabel,
|
||||
} from '../src/client/tool/models/terminal-card-model.ts'
|
||||
import { GenericToolCard, type GenericToolCardProps } from '../src/client/tool/toolviews/GenericToolCard.tsx'
|
||||
import { BashRow } from '../src/client/tool/toolviews/bash-sample.tsx'
|
||||
@@ -67,6 +67,40 @@ const settled = (over?: Partial<ToolResultNode>): ToolResultNode => {
|
||||
}
|
||||
|
||||
describe('terminalCardModel', () => {
|
||||
it.each([1, -1])('preserves signed exit code %s in the localized capsule', (code) => {
|
||||
const card = terminalCardModel(settled({ content: [{ type: 'text', text: `failed\n[exit code: ${code}]` }] }))
|
||||
expect(card?.card).toMatchObject({ output: 'failed', exitCode: code })
|
||||
expect(terminalExitLabel(card, enT)).toBe(`exit code ${code}`)
|
||||
expect(terminalExitLabel(card, t)).toBe(`退出码 ${code}`)
|
||||
})
|
||||
|
||||
it('omits exit capsules without a settled foreground failure', () => {
|
||||
expect(terminalExitLabel(null, enT)).toBeNull()
|
||||
expect(terminalExitLabel(undefined, enT)).toBeNull()
|
||||
expect(terminalExitLabel(terminalCardModel(running()), enT)).toBeNull()
|
||||
expect(terminalExitLabel(terminalCardModel(settled()), enT)).toBeNull()
|
||||
expect(terminalExitLabel(terminalCardModel(settled({
|
||||
call: { name: 'terminal_send', argsRaw: JSON.stringify({ sessionId: 'term-1', text: 'hello' }) },
|
||||
})), enT)).toBeNull()
|
||||
expect(terminalExitLabel(terminalCardModel(settled({ call: { name: 'bash', argsRaw: shellArgs({ run_in_background: true }) } })), enT)).toBeNull()
|
||||
})
|
||||
|
||||
it('names a recorded signal without substituting an exit code', () => {
|
||||
const model = terminalCardModel(settled({ content: [{ type: 'text', text: 'gone\n[killed by signal: SIGTERM]' }] }))
|
||||
expect(terminalExitLabel(model, enT)).toBe('signal SIGTERM')
|
||||
})
|
||||
|
||||
it.each(['bash', 'pwsh'])('preserves a %s timeout without an exit code', (name) => {
|
||||
const model = terminalCardModel(settled({
|
||||
call: { name, argsRaw: ARGS },
|
||||
content: [{ type: 'text', text: '(no output)\n[timed out after 100ms]\n[exit code: null]' }],
|
||||
}))!
|
||||
expect(model.card).toMatchObject({ output: '(no output)\n[timed out after 100ms]', exitCode: null })
|
||||
expect(terminalFailed(model)).toBe(true)
|
||||
expect(terminalExitLabel(model, enT)).toBe('no exit code')
|
||||
expect(terminalExitLabel(model, t)).toBe('未正常退出')
|
||||
})
|
||||
|
||||
it('derives a running standard-shell card from raw arguments', () => {
|
||||
expect(terminalCardModel(running({ argsRaw: shellArgs({ workdir: '/projects/app' }) }))).toEqual({
|
||||
copy: { kind: 'shell', command: 'ls -la', description: 'List files' },
|
||||
@@ -455,6 +489,9 @@ describe('chat row terminal body', () => {
|
||||
content: [{ type: 'text', text: 'boom\n[exit code: 2]' }],
|
||||
}))} />)
|
||||
expect(view.container.querySelector('[data-state]')?.getAttribute('data-state')).toBe('error')
|
||||
expect(view.getByText('退出码 2')).toBeTruthy()
|
||||
expect(view.getByText('退出码 2').previousElementSibling?.textContent).toBe('List files')
|
||||
expect(view.container.querySelector('[class*="_errorSummary_"]')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -512,7 +549,12 @@ describe('BashRow terminal card', () => {
|
||||
content: [{ type: 'text', text: 'boom\n[exit code: 2]' }],
|
||||
}))} />)
|
||||
expect(view.container.querySelector('[data-variant="bash"]')?.getAttribute('data-state')).toBe('error')
|
||||
expect(view.container.querySelector('[class*="_errorSummary_"]')?.textContent).toBe('List files')
|
||||
expect(view.container.querySelector('[class*="_errorSummary_"]')).toBeNull()
|
||||
expect(view.getByText('退出码 2')).toBeTruthy()
|
||||
expect(view.getByText('List files')).toBeTruthy()
|
||||
fireEvent.keyDown(view.getByRole('button'), { key: 'Enter' })
|
||||
expect(view.getAllByText('退出码 2')).toHaveLength(2)
|
||||
expect(view.getByText('boom', RAW)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows the call description as the terminal summary', () => {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
- 'button "Think There''s a `packages` directory. Let me look inside to see what packages are available."'
|
||||
- button "Bash List packages directory contents"
|
||||
- button "Think There are 4 packages:"
|
||||
- button "Failed Bash Randomly pick one package directory"
|
||||
- button "Failed Bash Randomly pick one package directory exit code 127"
|
||||
- 'button "Think shuf is not available on macOS. I''ll use a different approach for random selection. Let me use $RANDOM in bash or use `sort -R` or Python."'
|
||||
- button "Bash Randomly select one package with Python"
|
||||
- 'button "Think The random pick for turn 1 is `packages/context/session-reference`. Now I''ll output its file structure. Let me get the file tree."'
|
||||
|
||||
Reference in new issue
Block a user