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
12 changes: 12 additions & 0 deletions packages/app/e2e/performance/timeline-stability/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,18 @@ export function event(
return makeEvent(type, data)
}

export function toolInputStarted(data: Extract<OpenCodeEvent, { type: "session.tool.input.started" }>["data"]) {
return makeEvent("session.tool.input.started", data)
}

export function toolInputEnded(data: Extract<OpenCodeEvent, { type: "session.tool.input.ended" }>["data"]) {
return makeEvent("session.tool.input.ended", data)
}

export function toolCalled(data: Extract<OpenCodeEvent, { type: "session.tool.called" }>["data"]) {
return makeEvent("session.tool.called", data)
}

export function validateTimelineEvent(input: unknown): OpenCodeEvent {
if (!input || typeof input !== "object") throw new Error("Timeline event must be an object")
if (!("type" in input) || typeof input.type !== "string") throw new Error("Timeline event requires a type")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, test } from "@playwright/test"
import {
assistantID,
assistantMessage,
completedAssistantInfo,
messageUpdated,
Expand All @@ -8,9 +9,13 @@ import {
renderedPartID,
setupTimeline,
shell,
sessionID,
status,
stepStarted,
textPart,
toolCalled,
toolInputEnded,
toolInputStarted,
userMessage,
} from "../performance/timeline-stability/fixture"

Expand All @@ -34,6 +39,55 @@ for (const expanded of [false, true]) {
})
}

test("transitions a streaming shell from writing through command execution", async ({ page }) => {
const id = "prt_shell_streaming_input"
const command = "printf ready"
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistantMessage([], { completed: false })],
})
await timeline.send(toolInputStarted({ sessionID, assistantMessageID: assistantID, id, name: "shell" }))

const tool = page.locator(`[data-timeline-part-id="${id}"]`)
const title = tool.locator('[data-slot="basic-tool-tool-title"]')
const titleShimmer = title.locator('[data-component="text-shimmer"]')
const subtitle = tool.locator('[data-slot="basic-tool-tool-subtitle"]')
await expect(titleShimmer).toHaveAttribute("aria-label", "Shell")
await expect(titleShimmer).toHaveAttribute("data-active", "true")
await expect(subtitle).toHaveText("Writing command...")
await expect(subtitle.locator('[data-component="text-shimmer"]')).toHaveCount(0)
await expect(tool.locator('[data-component="shell-submessage"]')).toHaveCount(0)
await expect(tool.locator('[data-slot="collapsible-trigger"]')).toHaveCSS("height", "28px")
await expect(tool.locator('[data-component="tool-trigger"]')).toHaveCSS("gap", "6px")
await expect(title).toHaveCSS("font-size", "13px")
await expect(title).toHaveCSS("font-family", "Inter, sans-serif")
await expect(title).toHaveCSS("font-weight", "530")
await expect(title).toHaveCSS("line-height", "16px")
await expect(title).toHaveCSS("color", "rgb(22, 22, 22)")
await expect(subtitle).toHaveCSS("font-size", "13px")
await expect(subtitle).toHaveCSS("font-family", "Inter, sans-serif")
await expect(subtitle).toHaveCSS("font-weight", "440")
await expect(subtitle).toHaveCSS("line-height", "16px")
await expect(subtitle).toHaveCSS("color", "rgb(92, 92, 92)")

const input = JSON.stringify({ command })
await timeline.send(toolInputEnded({ sessionID, assistantMessageID: assistantID, id, text: input }))
await expect(titleShimmer).toHaveAttribute("data-active", "true")
await expect(subtitle).toHaveText(command)
await expect(tool).not.toContainText("Writing command...")

await timeline.send(
toolCalled({
sessionID,
assistantMessageID: assistantID,
id,
input: { command },
executed: true,
}),
)
await expect(titleShimmer).toHaveAttribute("data-active", "false")
await expect(subtitle).toHaveText(command)
})

test("shows and expands a running shell command without shimmering it", async ({ page }) => {
const id = "prt_shell_running_command"
const command = "sleep 10 && echo done"
Expand All @@ -43,9 +97,11 @@ test("shows and expands a running shell command without shimmering it", async ({
})

const tool = page.locator(`[data-timeline-part-id="${id}"]`)
await expect(tool.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "true")
await expect(tool.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "false")
await expect(tool).not.toContainText("Writing command...")
await expect(tool.locator('[data-component="shell-submessage"]')).toHaveText(command)
await expect(tool.locator('[data-component="shell-submessage"] [data-component="text-shimmer"]')).toHaveCount(0)
await expect(tool.locator('[data-slot="collapsible-trigger"]')).toHaveCSS("height", "28px")
await tool.locator('[data-slot="collapsible-trigger"]').click()
await expect(tool.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true")
await expect(tool.locator('[data-slot="bash-pre"]')).toContainText("still running")
Expand Down
25 changes: 25 additions & 0 deletions packages/session-ui/src/components/basic-tool.css
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,31 @@
}
}

[data-component="collapsible"].tool-collapsible[data-compact="true"] > [data-slot="collapsible-trigger"] {
height: 28px;

[data-component="tool-trigger"],
[data-slot="basic-tool-tool-info-main"] {
gap: 6px;
}

[data-slot="basic-tool-tool-title"] {
font-family: var(--v2-font-family-sans);
font-size: 13px;
font-weight: 530;
line-height: var(--v2-line-height-compact, 16px);
letter-spacing: -0.04px;
}

[data-slot="basic-tool-tool-subtitle"] {
font-family: var(--v2-font-family-sans);
font-size: 13px;
font-weight: 440;
line-height: var(--v2-line-height-compact, 16px);
letter-spacing: -0.04px;
}
}

[data-component="task-tool-card"] {
width: 100%;
min-width: 0;
Expand Down
2 changes: 2 additions & 0 deletions packages/session-ui/src/components/basic-tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface BasicToolProps {
triggerHref?: string
triggerAsLink?: boolean
clickable?: boolean
compact?: boolean
}

const SPRING = { type: "spring" as const, visualDuration: 0.35, bounce: 0 }
Expand Down Expand Up @@ -260,6 +261,7 @@ export function BasicTool(props: BasicToolProps) {
open={open()}
onOpenChange={props.locked ? undefined : handleOpenChange}
class="tool-collapsible"
data-compact={props.compact ? "true" : undefined}
data-rail={props.rail === false ? "false" : undefined}
>
<Show
Expand Down
21 changes: 15 additions & 6 deletions packages/session-ui/src/tools/tool-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1229,9 +1229,8 @@ ToolRegistry.register({
name: "shell",
render(props) {
const i18n = useI18n()
const pending = () =>
props.status === "streaming" || props.status === "running" || props.metadata.status === "running"
const sawPending = pending()
const streaming = () => props.status === "streaming"
const sawStreaming = streaming()
const command = () => {
if (typeof props.input.command === "string") return props.input.command
if (typeof props.metadata.command === "string") return props.metadata.command
Expand All @@ -1246,15 +1245,25 @@ ToolRegistry.register({
{...props}
icon="console"
rail={false}
compact
allowOpenWhilePending
trigger={(open) => (
<div data-slot="basic-tool-tool-info-structured">
<div data-slot="basic-tool-tool-info-main">
<span data-slot="basic-tool-tool-title">
<TextShimmer text={i18n.t("ui.tool.shell")} active={pending()} />
<TextShimmer text={i18n.t("ui.tool.shell")} active={streaming()} />
</span>
<Show when={!open() && command()}>
<ShellSubmessage text={command()} animate={sawPending} />
<Show when={!open()}>
<Show
when={command()}
fallback={
<Show when={streaming()}>
<span data-slot="basic-tool-tool-subtitle">{i18n.t("ui.tool.shell.writingCommand")}</span>
</Show>
}
>
{(command) => <ShellSubmessage text={command()} animate={sawStreaming} />}
</Show>
</Show>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ const source = {
"ui.tool.websearch": "Web Search",
"ui.tool.websearch.provider": "{{provider}} Web Search",
"ui.tool.shell": "Shell",
"ui.tool.shell.writingCommand": "Writing command...",
"ui.tool.execute": "Execute",
"ui.tool.patch": "Patch",
"ui.tool.questions": "Questions",
Expand Down
Loading