Skip to content
Open
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
36 changes: 36 additions & 0 deletions src/browser/features/Settings/Sections/GeneralSection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
DEFAULT_CODER_ARCHIVE_BEHAVIOR,
type CoderWorkspaceArchiveBehavior,
} from "@/common/config/coderArchiveBehavior";
import { TOOL_COLLAPSED_DISPLAY_MODE_KEY } from "@/common/constants/storage";
import {
DEFAULT_WORKTREE_ARCHIVE_BEHAVIOR,
type WorktreeArchiveBehavior,
Expand Down Expand Up @@ -302,6 +303,41 @@ describe("GeneralSection", () => {
});
}

async function waitForArchiveSettingsLoad(view: ReturnType<typeof render>): Promise<void> {
// Storage-only tests still need the async backend config load to settle before cleanup.
await waitFor(() => {
expect(getSelectTrigger(view, "Worktree archive behavior").textContent).toContain(
"Keep checkout"
);
});
}

test("updates the collapsed bash summary mode selection", async () => {
const { view } = renderGeneralSection();

expect(getSelectTrigger(view, "Collapsed bash summaries").textContent).toContain(
"Intent and command"
);

await chooseSelectOption(view, "Collapsed bash summaries", "Compact");

expect(window.localStorage.getItem(TOOL_COLLAPSED_DISPLAY_MODE_KEY)).toBe(
JSON.stringify("compact")
);
await waitForArchiveSettingsLoad(view);
});

test("falls back to the default collapsed bash summary mode for invalid storage", async () => {
window.localStorage.setItem(TOOL_COLLAPSED_DISPLAY_MODE_KEY, JSON.stringify("invalid"));

const { view } = renderGeneralSection();

expect(getSelectTrigger(view, "Collapsed bash summaries").textContent).toContain(
"Intent and command"
);
await waitForArchiveSettingsLoad(view);
});

test("renders the worktree archive behavior copy and loads the saved value", async () => {
const { view } = renderGeneralSection({
coderWorkspaceArchiveBehavior: "delete",
Expand Down
50 changes: 50 additions & 0 deletions src/browser/features/Settings/Sections/GeneralSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ import {
TERMINAL_FONT_CONFIG_KEY,
DEFAULT_TERMINAL_FONT_CONFIG,
LAUNCH_BEHAVIOR_KEY,
DEFAULT_TOOL_COLLAPSED_DISPLAY_MODE,
TOOL_COLLAPSED_DISPLAY_MODE_KEY,
isToolCollapsedDisplayMode,
type EditorConfig,
type EditorType,
type LaunchBehavior,
type TerminalFontConfig,
type ToolCollapsedDisplayMode,
} from "@/common/constants/storage";
import {
appendTerminalIconFallback,
Expand Down Expand Up @@ -143,6 +147,14 @@ const LAUNCH_BEHAVIOR_OPTIONS = [
{ value: "new-chat", label: "New chat on recent project" },
{ value: "last-workspace", label: "Last visited workspace" },
] as const;
const TOOL_COLLAPSED_DISPLAY_MODE_OPTIONS: Array<{
value: ToolCollapsedDisplayMode;
label: string;
}> = [
{ value: "intent-command", label: "Intent and command" },
{ value: "compact", label: "Compact" },
{ value: "command", label: "Command" },
];
const ARCHIVE_BEHAVIOR_OPTIONS = [
{ value: "keep", label: "Keep running" },
{ value: "stop", label: "Stop workspace" },
Expand All @@ -167,6 +179,13 @@ export function GeneralSection() {
LAUNCH_BEHAVIOR_KEY,
"dashboard"
);
const [rawToolCollapsedDisplayMode, setToolCollapsedDisplayMode] = usePersistedState<unknown>(
TOOL_COLLAPSED_DISPLAY_MODE_KEY,
DEFAULT_TOOL_COLLAPSED_DISPLAY_MODE
);
const toolCollapsedDisplayMode = isToolCollapsedDisplayMode(rawToolCollapsedDisplayMode)
? rawToolCollapsedDisplayMode
: DEFAULT_TOOL_COLLAPSED_DISPLAY_MODE;
const [rawTerminalFontConfig, setTerminalFontConfig] = usePersistedState<TerminalFontConfig>(
TERMINAL_FONT_CONFIG_KEY,
DEFAULT_TERMINAL_FONT_CONFIG
Expand Down Expand Up @@ -403,6 +422,12 @@ export function GeneralSection() {
setEditorConfig((prev) => ({ ...normalizeEditorConfig(prev), editor }));
};

const handleToolCollapsedDisplayModeChange = (value: string) => {
if (isToolCollapsedDisplayMode(value)) {
setToolCollapsedDisplayMode(value);
}
};

const handleTerminalFontFamilyChange = (fontFamily: string) => {
setTerminalFontConfig((prev) => ({ ...normalizeTerminalFontConfig(prev), fontFamily }));
};
Expand Down Expand Up @@ -502,6 +527,31 @@ export function GeneralSection() {
</Select>
</div>

<div className="flex items-center justify-between gap-4">
<div className="flex-1">
<div className="text-foreground text-sm">Collapsed bash summaries</div>
<div className="text-muted text-xs">
Choose whether collapsed bash tools show intent with the command, compact command
names, or the raw command.
</div>
</div>
<Select
value={toolCollapsedDisplayMode}
onValueChange={handleToolCollapsedDisplayModeChange}
>
<SelectTrigger className="border-border-medium bg-background-secondary hover:bg-hover h-9 w-auto cursor-pointer rounded-md border px-3 text-sm transition-colors">
<SelectValue />
</SelectTrigger>
<SelectContent>
{TOOL_COLLAPSED_DISPLAY_MODE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>

<div className="flex items-center justify-between gap-4">
<div className="flex-1">
<div className="text-foreground text-sm">Terminal Font</div>
Expand Down
185 changes: 124 additions & 61 deletions src/browser/features/Tools/Bash/BashToolCall.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import { BashBackgroundListToolCall } from "@/browser/features/Tools/BashBackgro
import { BashBackgroundTerminateToolCall } from "@/browser/features/Tools/BashBackgroundTerminateToolCall";
import { BashOutputToolCall } from "@/browser/features/Tools/BashOutputToolCall";
import { BashToolCall } from "@/browser/features/Tools/BashToolCall";
import { updatePersistedState } from "@/browser/hooks/usePersistedState";
import {
TOOL_COLLAPSED_DISPLAY_MODE_KEY,
type ToolCollapsedDisplayMode,
} from "@/common/constants/storage";
import { lightweightMeta } from "@/browser/stories/meta.js";

const meta = {
Expand All @@ -20,6 +25,10 @@ type Story = StoryObj<typeof meta>;

const STORYBOOK_WORKSPACE_ID = "storybook-bash";

function setCollapsedDisplayMode(mode: ToolCollapsedDisplayMode) {
updatePersistedState(TOOL_COLLAPSED_DISPLAY_MODE_KEY, mode);
}

function ToolStoryShell(props: { children: ReactNode }) {
return (
<BackgroundBashProvider workspaceId={STORYBOOK_WORKSPACE_ID}>
Expand Down Expand Up @@ -88,29 +97,32 @@ export const WithTerminal: Story = {

/** collapsed bash header showing intent on top and command on the second line */
export const IntentCollapsedSummary: Story = {
render: () => (
<ToolStoryShell>
<BashToolCall
workspaceId={STORYBOOK_WORKSPACE_ID}
toolCallId="intent-summary"
args={{
script: "sleep 30 && tail -30 /tmp/develop.log",
model_intent:
"Waiting for the dev instance to start using sleep 30 && tail -30 /tmp/develop.log for 30.1s",
run_in_background: false,
timeout_secs: 120,
display_name: "Dev server readiness",
}}
result={{
success: true,
output: "VITE ready on the sandbox frontend. Backend health check passed.",
exitCode: 0,
wall_duration_ms: 30_100,
}}
status="completed"
/>
</ToolStoryShell>
),
render: () => {
setCollapsedDisplayMode("intent-command");
return (
<ToolStoryShell>
<BashToolCall
workspaceId={STORYBOOK_WORKSPACE_ID}
toolCallId="intent-summary"
args={{
script: "sleep 30 && tail -30 /tmp/develop.log",
model_intent:
"Waiting for the dev instance to start using sleep 30 && tail -30 /tmp/develop.log for 30.1s",
run_in_background: false,
timeout_secs: 120,
display_name: "Dev server readiness",
}}
result={{
success: true,
output: "VITE ready on the sandbox frontend. Backend health check passed.",
exitCode: 0,
wall_duration_ms: 30_100,
}}
status="completed"
/>
</ToolStoryShell>
);
},
play: async ({ canvasElement }) => {
await waitFor(() => {
const textContent = canvasElement.textContent ?? "";
Expand All @@ -131,23 +143,26 @@ export const IntentCollapsedSummary: Story = {
};

export const IntentExecutingSummary: Story = {
render: () => (
<ToolStoryShell>
<BashToolCall
workspaceId={STORYBOOK_WORKSPACE_ID}
toolCallId="intent-executing-summary"
args={{
script: "sleep 30 && tail -30 /tmp/develop.log",
model_intent: "Waiting for the dev instance to start",
run_in_background: false,
timeout_secs: 120,
display_name: "Dev server readiness",
}}
status="executing"
startedAt={Date.now() - 1_000}
/>
</ToolStoryShell>
),
render: () => {
setCollapsedDisplayMode("intent-command");
return (
<ToolStoryShell>
<BashToolCall
workspaceId={STORYBOOK_WORKSPACE_ID}
toolCallId="intent-executing-summary"
args={{
script: "sleep 30 && tail -30 /tmp/develop.log",
model_intent: "Waiting for the dev instance to start",
run_in_background: false,
timeout_secs: 120,
display_name: "Dev server readiness",
}}
status="executing"
startedAt={Date.now() - 1_000}
/>
</ToolStoryShell>
);
},
play: async ({ canvasElement }) => {
await waitFor(() => {
const textContent = canvasElement.textContent ?? "";
Expand All @@ -164,29 +179,77 @@ export const IntentExecutingSummary: Story = {
},
};

export const CompactCollapsedSummary: Story = {
render: () => {
setCollapsedDisplayMode("compact");
return (
<ToolStoryShell>
<BashToolCall
workspaceId={STORYBOOK_WORKSPACE_ID}
toolCallId="compact-summary"
args={{
script: "sleep 30 && tail -30 /tmp/develop.log",
model_intent: "Waiting for the dev instance to start",
run_in_background: false,
timeout_secs: 120,
display_name: "Dev server readiness",
}}
result={{
success: true,
output: "VITE ready on the sandbox frontend. Backend health check passed.",
exitCode: 0,
wall_duration_ms: 30_100,
}}
status="completed"
/>
</ToolStoryShell>
);
},
play: async ({ canvasElement }) => {
await waitFor(() => {
const textContent = canvasElement.textContent ?? "";
if (!textContent.includes("sleep") || !textContent.includes("tail")) {
throw new Error("Compact mode should show command names");
}
if (textContent.includes("Waiting for the dev instance to start")) {
throw new Error("Compact mode should not show model intent text");
}
if (textContent.includes("sleep 30 && tail -30 /tmp/develop.log")) {
throw new Error("Compact mode should not echo the full script");
}
if (textContent.includes("timeout: 120s") || textContent.includes("took 30s")) {
throw new Error("Compact mode should not show command metadata");
}
});
},
};

/** when the model omits `model_intent`, the collapsed row falls back to the bare command */
export const NoIntentSummary: Story = {
render: () => (
<ToolStoryShell>
<BashToolCall
workspaceId={STORYBOOK_WORKSPACE_ID}
toolCallId="command-summary"
args={{
script: "sleep 30 && tail -30 /tmp/develop.log",
run_in_background: false,
timeout_secs: 120,
display_name: "Dev server readiness",
}}
result={{
success: true,
output: "VITE ready on the sandbox frontend. Backend health check passed.",
exitCode: 0,
wall_duration_ms: 30_100,
}}
status="completed"
/>
</ToolStoryShell>
),
render: () => {
setCollapsedDisplayMode("intent-command");
return (
<ToolStoryShell>
<BashToolCall
workspaceId={STORYBOOK_WORKSPACE_ID}
toolCallId="command-summary"
args={{
script: "sleep 30 && tail -30 /tmp/develop.log",
run_in_background: false,
timeout_secs: 120,
display_name: "Dev server readiness",
}}
result={{
success: true,
output: "VITE ready on the sandbox frontend. Backend health check passed.",
exitCode: 0,
wall_duration_ms: 30_100,
}}
status="completed"
/>
</ToolStoryShell>
);
},
play: async ({ canvasElement }) => {
await waitFor(() => {
const textContent = canvasElement.textContent ?? "";
Expand Down
Loading
Loading