From 9b5b001d1faa69771d2c75156d85c8f1a6ee51ac Mon Sep 17 00:00:00 2001 From: wsp Date: Tue, 1 Sep 2026 19:27:34 +0800 Subject: [PATCH] fix(toolcard): refine exec process card layout Keep the compact output frame stable during streaming and automatic collapse, while sizing manually expanded completed cards to their actual output. - Align WriteStdin execution metadata to the footer end - Hide ExecCommand TTY metadata and place WriteStdin exit code last - Replace stdin-specific action copy with user-facing input wording - Cover adaptive output sizing and WriteStdin footer ordering --- .../tool-cards/CommandToolCard.meta.ts | 1 + .../tool-cards/CommandToolCard.module.css | 16 +++++ .../flow-chat/tool-cards/CommandToolCard.tsx | 5 ++ .../ExecProcessToolCardView.test.tsx | 58 +++++++++++++++++++ .../tool-cards/ExecProcessToolCardView.tsx | 54 ++++++++++++----- src/web-ui/src/locales/en-US/flow-chat.json | 2 +- src/web-ui/src/locales/zh-CN/flow-chat.json | 2 +- src/web-ui/src/locales/zh-TW/flow-chat.json | 2 +- 8 files changed, 121 insertions(+), 19 deletions(-) diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.meta.ts b/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.meta.ts index ed2a007251..defbfddafb 100644 --- a/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.meta.ts +++ b/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.meta.ts @@ -11,6 +11,7 @@ export const commandToolCardMeta = { { name: "command", type: "string | null" }, { name: "isExpanded", type: "boolean" }, { name: "output", type: "ReactNode" }, + { defaultValue: "fixed", name: "outputSizing", type: "content | fixed" }, { defaultValue: "false", name: "reserveOutput", type: "boolean" }, { defaultValue: "false", name: "reserveFooter", type: "boolean" }, { name: "footerItems", type: "readonly CommandToolCardFooterItem[]" }, diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.module.css b/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.module.css index f683ad74d8..366f6d0bde 100644 --- a/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.module.css +++ b/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.module.css @@ -84,6 +84,18 @@ block-size: 20rem; } + .outputFrame[data-sizing="content"] { + block-size: auto; + } + + .outputFrame[data-density="compact"][data-sizing="content"] { + max-block-size: 5.375rem; + } + + .outputFrame[data-density="expanded"][data-sizing="content"] { + max-block-size: 20rem; + } + .output, .waiting { box-sizing: border-box; @@ -152,6 +164,10 @@ flex: 1 1 12rem; } + .footerItem[data-push-to-end="true"] { + margin-inline-start: auto; + } + .footerLabel { color: var(--bf-color-content-muted); font-size: var(--bf-font-size-sm); diff --git a/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.tsx b/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.tsx index ac61ec6e6b..0de7b14729 100644 --- a/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.tsx +++ b/design-system/packages/ui/src/flow-chat/tool-cards/CommandToolCard.tsx @@ -37,6 +37,7 @@ export interface CommandToolCardFooterItem { grow?: boolean; label?: ReactNode; monospace?: boolean; + pushToEnd?: boolean; tone?: "danger" | "neutral" | "success" | "warning"; value: ReactNode; } @@ -57,6 +58,7 @@ export interface CommandToolCardProps output?: ReactNode; outputAction?: ReactNode; outputDensity?: "compact" | "expanded"; + outputSizing?: "content" | "fixed"; reserveFooter?: boolean; reserveOutput?: boolean; requiresConfirmation?: boolean; @@ -91,6 +93,7 @@ export function CommandToolCard({ output, outputAction, outputDensity = "expanded", + outputSizing = "fixed", reserveFooter = false, reserveOutput = false, requiresConfirmation = false, @@ -148,6 +151,7 @@ export function CommandToolCard({ className={styles.outputFrame} data-bf-part="outputFrame" data-density={outputDensity} + data-sizing={outputSizing} > {outputAction && {outputAction}} {output @@ -162,6 +166,7 @@ export function CommandToolCard({ className={styles.footerItem} data-grow={item.grow ? "true" : "false"} data-monospace={item.monospace ? "true" : "false"} + data-push-to-end={item.pushToEnd ? "true" : "false"} data-tone={item.tone ?? "neutral"} key={index} > diff --git a/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.test.tsx b/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.test.tsx index 63374273cf..b1e1fc149e 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.test.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.test.tsx @@ -225,6 +225,63 @@ describe('ExecProcessToolCardView', () => { }); expect(container.querySelector('[data-bf-part="output"] pre')?.getAttribute('data-max-rows')).toBe('15'); + expect(container.querySelector('[data-bf-part="outputFrame"]')?.getAttribute('data-density')).toBe('expanded'); + expect(container.querySelector('[data-bf-part="outputFrame"]')?.getAttribute('data-sizing')).toBe('content'); + }); + + it('content-sizes a manually expanded completed card with no output', () => { + act(() => { + root.render( + , + ); + }); + + act(() => { + container + .querySelector('[data-bf-part="surface"][data-bf-attention="prominent"]') + ?.click(); + }); + + expect(container.textContent).toContain('No output'); + expect(container.querySelector('[data-bf-part="outputFrame"]')?.getAttribute('data-density')).toBe('expanded'); + expect(container.querySelector('[data-bf-part="outputFrame"]')?.getAttribute('data-sizing')).toBe('content'); + }); + + it('pushes WriteStdin session and execution metadata to the footer end', () => { + const stdinModel: ExecProcessCardModel = { + ...model, + kind: 'stdin', + sessionId: 42, + exitCode: 0, + wallTimeSeconds: 1.25, + }; + + act(() => { + root.render( + , + ); + }); + + act(() => { + container + .querySelector('[data-bf-part="surface"][data-bf-attention="prominent"]') + ?.click(); + }); + + const footerItems = Array.from(container.querySelectorAll('[data-bf-part="footer"] > span')); + expect(footerItems).toHaveLength(3); + expect(footerItems[0]?.getAttribute('data-push-to-end')).toBe('true'); + expect(footerItems[0]?.textContent).toContain('#42'); + expect(footerItems[1]?.getAttribute('data-push-to-end')).toBe('false'); + expect(footerItems[1]?.textContent).toContain('toolCards.execProcess.wallTime'); + expect(footerItems[2]?.getAttribute('data-push-to-end')).toBe('false'); + expect(footerItems[2]?.textContent).toContain('Exit code: 0'); }); it('keeps the output frame and footer mounted while content changes', () => { @@ -246,6 +303,7 @@ describe('ExecProcessToolCardView', () => { const frameBeforeOutput = container.querySelector('[data-bf-component="command-tool-card"] [data-bf-part="outputFrame"]'); const footerBeforeOutput = container.querySelector('[data-bf-component="command-tool-card"] [data-bf-part="footer"]'); expect(frameBeforeOutput?.getAttribute('data-density')).toBe('compact'); + expect(frameBeforeOutput?.getAttribute('data-sizing')).toBe('fixed'); expect(footerBeforeOutput?.textContent).toBe(''); expect(container.querySelector('[data-bf-part="output"] pre')).toBeNull(); diff --git a/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.tsx b/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.tsx index 93e4459aa1..7ac6f75778 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.tsx @@ -261,6 +261,7 @@ export const ExecProcessToolCardView: React.FC = ( return undefined; })(); const footerItems: CommandToolCardFooterItem[] = []; + const footerMetadataItems: CommandToolCardFooterItem[] = []; if (rejectedOrCancelled) { footerItems.push({ tone: 'warning', value: t(cancelledStatusLabelKey) }); @@ -273,31 +274,51 @@ export const ExecProcessToolCardView: React.FC = ( }); } if (model.sessionId != null) { - footerItems.push({ + footerMetadataItems.push({ label: t('toolCards.execProcess.session'), monospace: true, value: `#${model.sessionId}`, }); } if (model.remote) { - footerItems.push({ value: t('toolCards.execProcess.remote') }); + footerMetadataItems.push({ value: t('toolCards.execProcess.remote') }); } - if (model.tty) { - footerItems.push({ value: t('toolCards.execProcess.tty') }); + if (model.tty && model.kind !== 'command') { + footerMetadataItems.push({ value: t('toolCards.execProcess.tty') }); } - if (model.exitCode != null) { - footerItems.push({ - monospace: true, - tone: model.exitCode === 0 ? 'success' : 'danger', - value: t('toolCards.terminal.exitCode', { code: model.exitCode }), - }); - } - if (model.wallTimeSeconds != null) { - footerItems.push({ - monospace: true, - value: t('toolCards.execProcess.wallTime', { seconds: model.wallTimeSeconds.toFixed(3) }), - }); + const exitCodeFooterItem: CommandToolCardFooterItem | undefined = model.exitCode != null + ? { + monospace: true, + tone: model.exitCode === 0 ? 'success' : 'danger', + value: t('toolCards.terminal.exitCode', { code: model.exitCode }), + } + : undefined; + const wallTimeFooterItem: CommandToolCardFooterItem | undefined = model.wallTimeSeconds != null + ? { + monospace: true, + value: t('toolCards.execProcess.wallTime', { seconds: model.wallTimeSeconds.toFixed(3) }), + } + : undefined; + if (model.kind === 'stdin') { + if (wallTimeFooterItem) { + footerMetadataItems.push(wallTimeFooterItem); + } + if (exitCodeFooterItem) { + footerMetadataItems.push(exitCodeFooterItem); + } + } else { + if (exitCodeFooterItem) { + footerMetadataItems.push(exitCodeFooterItem); + } + if (wallTimeFooterItem) { + footerMetadataItems.push(wallTimeFooterItem); + } } + footerItems.push(...footerMetadataItems.map((item, index) => ( + model.kind === 'stdin' && index === 0 + ? { ...item, pushToEnd: true } + : item + ))); return (
@@ -328,6 +349,7 @@ export const ExecProcessToolCardView: React.FC = ( ) : undefined} outputAction={outputText ? renderCopyOutputButton() : undefined} outputDensity={keepCompactCompletionPreview || isRunning ? 'compact' : 'expanded'} + outputSizing={status === 'completed' && userToggledRef.current ? 'content' : 'fixed'} reserveFooter reserveOutput requiresConfirmation={status === 'pending_confirmation'} diff --git a/src/web-ui/src/locales/en-US/flow-chat.json b/src/web-ui/src/locales/en-US/flow-chat.json index b1dcf49d1a..7e28b50e20 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -1749,7 +1749,7 @@ }, "execProcess": { "executeCommand": "Run command:", - "writeStdin": "Write stdin:", + "writeStdin": "Send input:", "pollProcess": "Poll process:", "interruptProcess": "Interrupt process:", "killProcess": "Kill process:", diff --git a/src/web-ui/src/locales/zh-CN/flow-chat.json b/src/web-ui/src/locales/zh-CN/flow-chat.json index cda5de0052..e62a38b863 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -1749,7 +1749,7 @@ }, "execProcess": { "executeCommand": "运行命令:", - "writeStdin": "写入标准输入:", + "writeStdin": "发送输入:", "pollProcess": "轮询进程:", "interruptProcess": "中断进程:", "killProcess": "终止进程:", diff --git a/src/web-ui/src/locales/zh-TW/flow-chat.json b/src/web-ui/src/locales/zh-TW/flow-chat.json index 45fe7f26ca..86c5e2b511 100644 --- a/src/web-ui/src/locales/zh-TW/flow-chat.json +++ b/src/web-ui/src/locales/zh-TW/flow-chat.json @@ -1749,7 +1749,7 @@ }, "execProcess": { "executeCommand": "執行命令:", - "writeStdin": "寫入標準輸入:", + "writeStdin": "傳送輸入:", "pollProcess": "輪詢進程:", "interruptProcess": "中斷進程:", "killProcess": "終止進程:",