Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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[]" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface CommandToolCardFooterItem {
grow?: boolean;
label?: ReactNode;
monospace?: boolean;
pushToEnd?: boolean;
tone?: "danger" | "neutral" | "success" | "warning";
value: ReactNode;
}
Expand All @@ -57,6 +58,7 @@ export interface CommandToolCardProps
output?: ReactNode;
outputAction?: ReactNode;
outputDensity?: "compact" | "expanded";
outputSizing?: "content" | "fixed";
reserveFooter?: boolean;
reserveOutput?: boolean;
requiresConfirmation?: boolean;
Expand Down Expand Up @@ -91,6 +93,7 @@ export function CommandToolCard({
output,
outputAction,
outputDensity = "expanded",
outputSizing = "fixed",
reserveFooter = false,
reserveOutput = false,
requiresConfirmation = false,
Expand Down Expand Up @@ -148,6 +151,7 @@ export function CommandToolCard({
className={styles.outputFrame}
data-bf-part="outputFrame"
data-density={outputDensity}
data-sizing={outputSizing}
>
{outputAction && <span className={styles.outputActions}>{outputAction}</span>}
{output
Expand All @@ -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}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ExecProcessToolCardView
toolItem={toolItem('completed')}
model={model}
/>,
);
});

act(() => {
container
.querySelector<HTMLElement>('[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(
<ExecProcessToolCardView
toolItem={{ ...toolItem('completed'), toolName: 'WriteStdin' }}
model={stdinModel}
/>,
);
});

act(() => {
container
.querySelector<HTMLElement>('[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', () => {
Expand All @@ -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();

Expand Down
54 changes: 38 additions & 16 deletions src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ export const ExecProcessToolCardView: React.FC<ExecProcessToolCardViewProps> = (
return undefined;
})();
const footerItems: CommandToolCardFooterItem[] = [];
const footerMetadataItems: CommandToolCardFooterItem[] = [];

if (rejectedOrCancelled) {
footerItems.push({ tone: 'warning', value: t(cancelledStatusLabelKey) });
Expand All @@ -273,31 +274,51 @@ export const ExecProcessToolCardView: React.FC<ExecProcessToolCardViewProps> = (
});
}
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 (
<div ref={cardRootRef} data-bf-adapter="exec-process-tool-card" data-tool-card-id={toolId ?? ''}>
Expand Down Expand Up @@ -328,6 +349,7 @@ export const ExecProcessToolCardView: React.FC<ExecProcessToolCardViewProps> = (
) : undefined}
outputAction={outputText ? renderCopyOutputButton() : undefined}
outputDensity={keepCompactCompletionPreview || isRunning ? 'compact' : 'expanded'}
outputSizing={status === 'completed' && userToggledRef.current ? 'content' : 'fixed'}
reserveFooter
reserveOutput
requiresConfirmation={status === 'pending_confirmation'}
Expand Down
2 changes: 1 addition & 1 deletion src/web-ui/src/locales/en-US/flow-chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -1749,7 +1749,7 @@
},
"execProcess": {
"executeCommand": "Run command:",
"writeStdin": "Write stdin:",
"writeStdin": "Send input:",
"pollProcess": "Poll process:",
"interruptProcess": "Interrupt process:",
"killProcess": "Kill process:",
Expand Down
2 changes: 1 addition & 1 deletion src/web-ui/src/locales/zh-CN/flow-chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -1749,7 +1749,7 @@
},
"execProcess": {
"executeCommand": "运行命令:",
"writeStdin": "写入标准输入:",
"writeStdin": "发送输入:",
"pollProcess": "轮询进程:",
"interruptProcess": "中断进程:",
"killProcess": "终止进程:",
Expand Down
2 changes: 1 addition & 1 deletion src/web-ui/src/locales/zh-TW/flow-chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -1749,7 +1749,7 @@
},
"execProcess": {
"executeCommand": "執行命令:",
"writeStdin": "寫入標準輸入:",
"writeStdin": "傳送輸入:",
"pollProcess": "輪詢進程:",
"interruptProcess": "中斷進程:",
"killProcess": "終止進程:",
Expand Down
Loading