feat(app): refine expanded shell output (#44368)

This commit is contained in:
Luke Parker
2026-08-23 21:31:40 +10:00
committed by GitHub
parent 3b5b5aee0c
commit d7c3a95368
10 changed files with 92 additions and 18 deletions
+1
View File
@@ -54,6 +54,7 @@
"name": "@opencode-ai/app",
"version": "1.18.15",
"dependencies": {
"@ibm/plex": "6.4.1",
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
"@dnd-kit/dom": "0.5.0",
@@ -358,7 +358,8 @@ test.describe("smoke: session timeline", () => {
const shellTrigger = shell.locator('[data-slot="collapsible-trigger"]')
const shellSubtitle = shell.locator('[data-slot="basic-tool-tool-subtitle"]')
await expect(shellSubtitle).toHaveCount(0)
await expect(shell.locator('[data-slot="bash-pre"]')).toContainText("$ bun typecheck")
await expect(shell.locator('[data-slot="bash-command"]')).toHaveText("bun typecheck")
await expect(shell.locator('[data-slot="bash-result"]')).not.toContainText("bun typecheck")
await shellTrigger.click()
await expect(shellTrigger).toHaveAttribute("aria-expanded", "false")
await expect(shellSubtitle).toHaveText("bun typecheck")
+1
View File
@@ -52,6 +52,7 @@
"vite-plugin-solid": "2.11.14"
},
"dependencies": {
"@ibm/plex": "6.4.1",
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
"@dnd-kit/dom": "0.5.0",
+8
View File
@@ -17,6 +17,14 @@
font-style: normal;
}
@font-face {
font-family: "IBM Plex Mono";
src: url("@ibm/plex/IBM-Plex-Mono/fonts/complete/woff2/IBMPlexMono-Text.woff2") format("woff2");
font-weight: 440;
font-style: normal;
font-display: swap;
}
@media (display-mode: standalone) {
/* WebKit excludes safe-area insets from dvh in installed apps. */
#root {
+1
View File
@@ -43,6 +43,7 @@ export default defineConfig({
"web-app-manifest*",
"assets/index-*.{js,css}",
"assets/session-*.js",
"assets/IBMPlexMono-Text-*.woff2",
"assets/Inter.ttf",
"assets/JetBrainsMonoNerdFontMono-Regular.woff2",
],
@@ -419,11 +419,44 @@
overflow-wrap: anywhere;
}
[data-slot="bash-prompt"] {
user-select: none;
&[data-variant="shell"] {
background: var(--v2-background-bg-base);
[data-slot="bash-pre"] {
padding: 0;
}
[data-slot="bash-pre"] code {
font-family: var(--v2-font-family-code);
font-weight: 440;
line-height: var(--line-height-base);
letter-spacing: -0.04px;
}
[data-slot="bash-command"],
[data-slot="bash-result"] {
display: block;
padding: 12px;
}
[data-slot="bash-result"] {
border-block-start: 0.5px solid var(--v2-border-border-base);
background: var(--v2-background-bg-layer-01);
color: var(--v2-text-text-muted);
}
[data-slot="bash-command"] {
color: var(--v2-text-text-base);
}
}
}
[data-component="collapsible"].tool-collapsible:has(
> [data-slot="collapsible-content"] [data-component="bash-output"][data-variant="shell"]
) {
--tool-content-gap: 6px;
}
[data-component="edit-trigger"],
[data-component="write-trigger"] {
display: flex;
@@ -507,6 +507,29 @@ export const terminalPassedDocument = document([
}),
] satisfies SessionMessageInfo[])
export const expandedShellDocument = document([
user("msg_user_shell_expanded", "Inspect the active service and its available feature surface.", 52_100),
assistant({
id: "msg_assistant_shell_expanded",
offset: 52_200,
completed: 52_900,
content: [
completedTool({
id: "tool_shell_expanded",
name: "shell",
offset: 52_300,
args: {
command:
"opencode2 api get /openapi.json > /private/var/folders/j/gd69b2|16y91666jzf3p9g22asdasc0000gn/T/opencode/temp/tes",
},
output:
'{"location":{"directory":"/Users/usrnk1","project":{"id":"global","directory":"/","canonical":"/"}},"data":[{"id":"opencode","name":"OpenCode","description":"Use this skill for any question about OpenCode itself, including how OpenCode works, using or configuring it, migrating from V1 to V2, troubleshooting it, developing plugins or integrations, using the OpenCode SDK, clients, server, or API, and contributing to the OpenCode codebase."}]}',
metadata: { exit: 0 },
}),
],
}),
] satisfies SessionMessageInfo[])
export const terminalFailedDocument = document([
user("msg_user_terminal_failed", "Run the focused Session UI tests.", 53_000),
assistant({
@@ -1,5 +1,6 @@
import { CurrentSessionTimelineStory } from "../storybook/current-session-story"
import {
expandedShellDocument,
recoveryDocument,
standaloneShellCompletedDocument,
standaloneShellRunningDocument,
@@ -72,6 +73,18 @@ export const TestsPassed = {
),
}
export const ExpandedShell = {
render: () => (
<CurrentSessionTimelineStory
title="Expanded shell"
description="The expanded shell separates the command from its output in a full-width card without a detail rail."
document={expandedShellDocument}
width="786px"
shellToolDefaultOpen
/>
),
}
export const TestFailed = {
render: () => (
<CurrentSessionTimelineStory
@@ -1182,7 +1182,7 @@ ToolRegistry.register({
},
})
function ConsoleOutput(props: { copy: string; children: JSX.Element }) {
function ConsoleOutput(props: { copy: string; children: JSX.Element; variant?: "shell" }) {
const i18n = useI18n()
const [copied, setCopied] = createSignal(false)
@@ -1194,7 +1194,7 @@ function ConsoleOutput(props: { copy: string; children: JSX.Element }) {
}
return (
<div data-component="bash-output" dir="ltr">
<div data-component="bash-output" data-variant={props.variant} dir="ltr">
<div data-slot="bash-copy">
<Tooltip value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} placement="top">
<IconButton
@@ -1273,10 +1273,7 @@ ToolRegistry.register({
if (typeof props.metadata.command === "string") return props.metadata.command
return ""
}
const text = createMemo(() => {
const out = stripAnsi(props.output ?? "").replace(/\r\n?/g, "\n")
return `${command()}${out ? "\n\n" + out : ""}`
})
const output = createMemo(() => stripAnsi(props.output ?? "").replace(/\r\n?/g, "\n"))
return (
<BasicTool
{...props}
@@ -1288,10 +1285,7 @@ ToolRegistry.register({
<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={pending()} />
</span>
<Show when={!open()}>
<Show
@@ -1309,11 +1303,9 @@ ToolRegistry.register({
</div>
)}
>
<ConsoleOutput copy={command()}>
<span data-slot="bash-prompt" aria-hidden="true">
{"$ "}
</span>
{text()}
<ConsoleOutput copy={command()} variant="shell">
<span data-slot="bash-command">{command()}</span>
<Show when={output()}>{(value) => <span data-slot="bash-result">{value()}</span>}</Show>
</ConsoleOutput>
</BasicTool>
)
+1
View File
@@ -131,6 +131,7 @@
--font-family-text: "Inter", sans-serif;
--v2-font-family-sans: "Inter", sans-serif;
--v2-font-family-code: "IBM Plex Mono", var(--font-family-mono);
--line-height-tight: 12px;
--line-height-compact: 16px;
--line-height-base: 20px;