mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
fix(session-ui): wrap execute calls and show code mode output (#56915)
This commit is contained in:
committed by
GitHub
Unverified
parent
b110e4a20e
commit
b367a4b3ca
@@ -179,6 +179,31 @@ story("opens an execution's code and output from its View code action", async ({
|
||||
await expect(dialog.getByRole("button", { name: "Copy output" })).toBeVisible()
|
||||
})
|
||||
|
||||
story("keeps long execute calls inside the tool and shows the script output", async ({ mount }) => {
|
||||
const timeline = await mount("current-session-terminal-work--execute-code")
|
||||
const tool = timeline.locator('[data-timeline-part-id="tool_execute_long_call"]')
|
||||
const calls = tool.locator('[data-component="execute-tool-calls"]')
|
||||
const rows = calls.locator('[data-slot="execute-tool-call"]')
|
||||
await expect(rows).toHaveText([/mesh\.exec/, /Output\s*3 lines/])
|
||||
|
||||
// The script output stays collapsed until its row is expanded.
|
||||
const result = calls.locator('[data-slot="bash-result"]')
|
||||
await expect(result).toHaveCount(0)
|
||||
const top = await rows.first().evaluate((element) => element.getBoundingClientRect().top)
|
||||
await rows.nth(1).click()
|
||||
await expect(rows.nth(1)).toHaveAttribute("aria-expanded", "true")
|
||||
await expect(result).toContainText("Cleanup finished on blade")
|
||||
// Opening the output must not move the rows above it.
|
||||
expect(await rows.first().evaluate((element) => element.getBoundingClientRect().top)).toBe(top)
|
||||
|
||||
const overflow = await tool.evaluate((element) => {
|
||||
const calls = element.querySelector('[data-component="execute-tool-calls"]')
|
||||
if (!calls) return Number.NaN
|
||||
return calls.getBoundingClientRect().right - element.getBoundingClientRect().right
|
||||
})
|
||||
expect(overflow).toBeLessThanOrEqual(0)
|
||||
})
|
||||
|
||||
story("shows a failed execution's error in place of its output", async ({ mount }) => {
|
||||
const timeline = await mount("current-session-terminal-work--execute-failed")
|
||||
await timeline.getByRole("button", { name: /^Used 5 / }).click()
|
||||
|
||||
@@ -192,6 +192,8 @@
|
||||
|
||||
> [data-slot="collapsible-content"] {
|
||||
flex-basis: 100%;
|
||||
/* In this row layout, unbreakable content would otherwise widen the content past the tool's width. */
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -502,14 +502,20 @@
|
||||
color: var(--v2-text-text-muted);
|
||||
}
|
||||
|
||||
/* Code Mode results have no command line above them. */
|
||||
[data-slot="bash-result"]:first-child {
|
||||
border-block-start: 0;
|
||||
}
|
||||
|
||||
[data-slot="bash-command"] {
|
||||
color: var(--v2-text-text-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Only a shell's own card; an Execute output card opens inside the content and must not move the rows above it. */
|
||||
[data-component="collapsible"].tool-collapsible:has(
|
||||
> [data-slot="collapsible-content"] [data-component="bash-output"][data-variant="shell"]
|
||||
> [data-slot="collapsible-content"] > [data-component="bash-output"][data-variant="shell"]
|
||||
) {
|
||||
--tool-content-gap: 8px;
|
||||
}
|
||||
@@ -666,6 +672,11 @@
|
||||
color: var(--v2-state-fg-danger);
|
||||
}
|
||||
|
||||
[data-slot="execute-tool-output"] {
|
||||
margin-top: 4px;
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
|
||||
[data-slot="execute-tool-error"] {
|
||||
margin-top: 4px;
|
||||
max-height: 240px;
|
||||
|
||||
@@ -562,6 +562,29 @@ export const executeCodeDocument = document([
|
||||
output: "[]",
|
||||
metadata: { toolCalls: [{ tool: "browser.tabs.list", status: "completed" }] },
|
||||
}),
|
||||
completedTool({
|
||||
id: "tool_execute_long_call",
|
||||
name: "execute",
|
||||
offset: 52_700,
|
||||
args: {
|
||||
code: 'return await tools.mesh.exec({ target: "blade", script: "foreach ($t in \'\\\\opencode-mesh\\\\sync\', \'\\\\opencode-mesh\\\\run\') { schtasks /delete /tn $t /f 2>&1 | Out-Null; \\"deleted $t\\" }" })',
|
||||
},
|
||||
output:
|
||||
"deleted \\opencode-mesh\\sync\ndeleted \\opencode-mesh\\run\nCleanup finished on blade after removing both scheduled tasks and their trigger history.",
|
||||
metadata: {
|
||||
toolCalls: [
|
||||
{
|
||||
tool: "mesh.exec",
|
||||
status: "completed",
|
||||
input: {
|
||||
target: "blade",
|
||||
script:
|
||||
"foreach ($t in '\\opencode-mesh\\sync', '\\opencode-mesh\\run') { schtasks /delete /tn $t /f 2>&1 | Out-Null; \"deleted $t\" }",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
] satisfies SessionMessageInfo[])
|
||||
|
||||
@@ -1932,7 +1932,7 @@ ToolRegistry.register({
|
||||
const subtitle = createMemo(() => executeToolSubtitle(calls()))
|
||||
const code = createMemo(() => (toolInput(props.input).code ?? "").trimEnd())
|
||||
|
||||
// Like the TUI, only a runtime failure shows its output; a failed nested call is marked on its row.
|
||||
// A runtime failure shows its output on the failing row; a successful script shows its result below the calls.
|
||||
const error = createMemo(() => {
|
||||
if (props.status !== "error" && props.metadata.error !== true) return undefined
|
||||
const fallback = i18n.t("ui.toolErrorCard.failed")
|
||||
@@ -1941,6 +1941,14 @@ ToolRegistry.register({
|
||||
return text.replace(/^Error:\s*/, "").trim() || fallback
|
||||
})
|
||||
|
||||
const output = createMemo(() =>
|
||||
error()
|
||||
? ""
|
||||
: stripAnsi(props.output ?? "")
|
||||
.replace(/\r\n?/g, "\n")
|
||||
.trim(),
|
||||
)
|
||||
|
||||
// A runtime failure belongs to the call that raised it; scripts can also fail outside any call.
|
||||
const owner = createMemo(() => calls().findLastIndex((call) => call.status === "error"))
|
||||
|
||||
@@ -1953,7 +1961,7 @@ ToolRegistry.register({
|
||||
dialog.show(() => (
|
||||
<ExecuteDetails
|
||||
code={code()}
|
||||
output={stripAnsi(props.output ?? "").replace(/\r\n?/g, "\n").trim()}
|
||||
output={output()}
|
||||
error={error()}
|
||||
calls={calls().length}
|
||||
status={props.status}
|
||||
@@ -1965,7 +1973,7 @@ ToolRegistry.register({
|
||||
{...props}
|
||||
icon="console"
|
||||
rail={false}
|
||||
hasContent={calls().length > 0 || !!error()}
|
||||
hasContent={calls().length > 0 || !!error() || !!output()}
|
||||
compact
|
||||
allowOpenWhilePending
|
||||
trigger={(open) => (
|
||||
@@ -1998,6 +2006,7 @@ ToolRegistry.register({
|
||||
{(call, index) => <ExecuteCallRow call={call()} error={index === owner() ? error() : undefined} />}
|
||||
</Index>
|
||||
<Show when={owner() === -1 && error()}>{(text) => <ExecuteCallRow error={text()} />}</Show>
|
||||
<Show when={output()}>{(value) => <ExecuteOutputRow output={value()} />}</Show>
|
||||
</div>
|
||||
</BasicTool>
|
||||
)
|
||||
@@ -2120,6 +2129,49 @@ function ExecuteDetailsSection(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function ExecuteOutputRow(props: { output: string }) {
|
||||
const i18n = useI18n()
|
||||
const [open, setOpen] = createSignal(false)
|
||||
|
||||
const toggle = () => {
|
||||
if (window.getSelection()?.toString()) return
|
||||
setOpen((value) => !value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-slot="execute-tool-call-item">
|
||||
<div
|
||||
data-slot="execute-tool-call"
|
||||
data-kind="output"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-expanded={open()}
|
||||
onClick={toggle}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return
|
||||
event.preventDefault()
|
||||
toggle()
|
||||
}}
|
||||
>
|
||||
<span data-slot="execute-tool-call-marker">
|
||||
<span data-slot="execute-tool-call-arrow">›</span>
|
||||
</span>
|
||||
<span data-slot="execute-tool-call-name">{i18n.t("ui.tool.execute.output")}</span>
|
||||
<span data-slot="execute-tool-call-args">
|
||||
{i18n.plural("ui.tool.execute.lines", props.output.split("\n").length)}
|
||||
</span>
|
||||
</div>
|
||||
<Show when={open()}>
|
||||
<div data-slot="execute-tool-output">
|
||||
<ConsoleOutput copy={props.output} variant="shell">
|
||||
<span data-slot="bash-result">{props.output}</span>
|
||||
</ConsoleOutput>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ExecuteCallRow(props: { call?: ExecuteToolCall; error?: string }) {
|
||||
const [open, setOpen] = createSignal(false)
|
||||
const args = createMemo(() => (props.call ? executeToolCallArgs(props.call) : []))
|
||||
|
||||
Reference in New Issue
Block a user