|
|
@@ -547,6 +547,12 @@ export function getToolInfo(
|
|
|
title: i18n.t("ui.tool.shell"),
|
|
|
subtitle: input.command,
|
|
|
}
|
|
|
+ case "execute":
|
|
|
+ return {
|
|
|
+ icon: "console",
|
|
|
+ title: i18n.t("ui.tool.execute"),
|
|
|
+ subtitle: input.code,
|
|
|
+ }
|
|
|
case "edit":
|
|
|
return {
|
|
|
icon: "code-lines",
|
|
|
@@ -1574,6 +1580,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
|
|
if (typeof value === "string" && value) return value
|
|
|
return taskId()
|
|
|
})
|
|
|
+ const toolError = createMemo(() => partError(part(), i18n.t("ui.toolErrorCard.failed")))
|
|
|
|
|
|
const render = createMemo(() => ToolRegistry.render(part().tool) ?? GenericTool)
|
|
|
const controlledOpen = () => (props.onToolOpenChange ? (props.toolOpen ?? props.defaultOpen) : undefined)
|
|
|
@@ -1583,7 +1590,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
|
|
<Show when={!hideQuestion()}>
|
|
|
<div data-component="tool-part-wrapper" data-timeline-part-id={part().id}>
|
|
|
<Switch>
|
|
|
- <Match when={part().state.status === "error" && (part().state as any).error}>
|
|
|
+ <Match when={toolError()}>
|
|
|
{(error) => {
|
|
|
const cleaned = error().replace("Error: ", "")
|
|
|
if (part().tool === "question" && cleaned.includes("dismissed this question")) {
|
|
|
@@ -1644,6 +1651,26 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
+function partError(part: ToolPart, fallback: string) {
|
|
|
+ if (part.state.status === "error") return part.state.error
|
|
|
+ if (part.tool !== "execute" || !("metadata" in part.state)) return undefined
|
|
|
+ const calls = part.state.metadata?.toolCalls
|
|
|
+ const failed =
|
|
|
+ part.state.metadata?.error === true ||
|
|
|
+ (Array.isArray(calls) &&
|
|
|
+ calls.some(
|
|
|
+ (call) =>
|
|
|
+ call !== null &&
|
|
|
+ typeof call === "object" &&
|
|
|
+ !Array.isArray(call) &&
|
|
|
+ "status" in call &&
|
|
|
+ call.status === "error",
|
|
|
+ ))
|
|
|
+ if (!failed) return undefined
|
|
|
+ if ("output" in part.state && typeof part.state.output === "string" && part.state.output) return part.state.output
|
|
|
+ return fallback
|
|
|
+}
|
|
|
+
|
|
|
export function MessageDivider(props: { label: string }) {
|
|
|
return (
|
|
|
<div data-component="compaction-part">
|
|
|
@@ -2104,6 +2131,84 @@ ToolRegistry.register({
|
|
|
|
|
|
ToolRegistry.register({ name: "subagent", render: ToolRegistry.render("task") })
|
|
|
|
|
|
+function ConsoleOutput(props: { copy: string; children: JSX.Element }) {
|
|
|
+ const i18n = useI18n()
|
|
|
+ const [copied, setCopied] = createSignal(false)
|
|
|
+
|
|
|
+ const copy = async () => {
|
|
|
+ if (!props.copy) return
|
|
|
+ if (!(await writeClipboard(props.copy))) return
|
|
|
+ setCopied(true)
|
|
|
+ setTimeout(() => setCopied(false), 2000)
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div data-component="bash-output" dir="ltr">
|
|
|
+ <div data-slot="bash-copy">
|
|
|
+ <TooltipV2 value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} placement="top">
|
|
|
+ <IconButtonV2
|
|
|
+ icon={<IconV2 name={copied() ? "check" : "outline-copy"} size="small" />}
|
|
|
+ size="normal"
|
|
|
+ variant="ghost-muted"
|
|
|
+ onMouseDown={(event) => event.preventDefault()}
|
|
|
+ onClick={copy}
|
|
|
+ aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
|
|
|
+ />
|
|
|
+ </TooltipV2>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ data-slot="bash-scroll"
|
|
|
+ data-scrollable
|
|
|
+ tabIndex={0}
|
|
|
+ role="region"
|
|
|
+ aria-label={i18n.t("ui.scrollView.ariaLabel")}
|
|
|
+ >
|
|
|
+ <pre data-slot="bash-pre">
|
|
|
+ <code>{props.children}</code>
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+ToolRegistry.register({
|
|
|
+ name: "execute",
|
|
|
+ render(props) {
|
|
|
+ const i18n = useI18n()
|
|
|
+ const pending = () => props.status === "pending" || props.status === "streaming" || props.status === "running"
|
|
|
+ const code = createMemo(() => (typeof props.input.code === "string" ? props.input.code : ""))
|
|
|
+ const text = createMemo(() => {
|
|
|
+ const output = stripAnsi(props.output ?? "").replace(/\r\n?/g, "\n")
|
|
|
+ return `${code()}${output ? "\n\n" + output : ""}`
|
|
|
+ })
|
|
|
+ const sawPending = pending()
|
|
|
+ return (
|
|
|
+ <BasicTool
|
|
|
+ {...props}
|
|
|
+ icon="console"
|
|
|
+ allowOpenWhilePending
|
|
|
+ trigger={(open) => (
|
|
|
+ <div data-slot="basic-tool-tool-info-structured">
|
|
|
+ <span data-slot="basic-tool-tool-indicator">
|
|
|
+ <Icon name="console" size="small" />
|
|
|
+ </span>
|
|
|
+ <div data-slot="basic-tool-tool-info-main">
|
|
|
+ <span data-slot="basic-tool-tool-title">
|
|
|
+ <TextShimmer text={i18n.t("ui.tool.execute")} active={pending()} />
|
|
|
+ </span>
|
|
|
+ <Show when={!open() && code()}>
|
|
|
+ <ShellSubmessage text={code()} animate={sawPending} />
|
|
|
+ </Show>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ >
|
|
|
+ <ConsoleOutput copy={text()}>{text()}</ConsoleOutput>
|
|
|
+ </BasicTool>
|
|
|
+ )
|
|
|
+ },
|
|
|
+})
|
|
|
+
|
|
|
ToolRegistry.register({
|
|
|
name: "shell",
|
|
|
render(props) {
|
|
|
@@ -2116,17 +2221,6 @@ ToolRegistry.register({
|
|
|
const out = stripAnsi(props.output || props.metadata.output || "").replace(/\r\n?/g, "\n")
|
|
|
return `${command()}${out ? "\n\n" + out : ""}`
|
|
|
})
|
|
|
- const [copied, setCopied] = createSignal(false)
|
|
|
-
|
|
|
- const handleCopy = async () => {
|
|
|
- const content = command()
|
|
|
- if (!content) return
|
|
|
- if (await writeClipboard(content)) {
|
|
|
- setCopied(true)
|
|
|
- setTimeout(() => setCopied(false), 2000)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
return (
|
|
|
<BasicTool
|
|
|
{...props}
|
|
|
@@ -2145,36 +2239,12 @@ ToolRegistry.register({
|
|
|
</div>
|
|
|
)}
|
|
|
>
|
|
|
- <div data-component="bash-output" dir="ltr">
|
|
|
- <div data-slot="bash-copy">
|
|
|
- <TooltipV2 value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} placement="top">
|
|
|
- <IconButtonV2
|
|
|
- icon={<IconV2 name={copied() ? "check" : "outline-copy"} size="small" />}
|
|
|
- size="normal"
|
|
|
- variant="ghost-muted"
|
|
|
- onMouseDown={(e) => e.preventDefault()}
|
|
|
- onClick={handleCopy}
|
|
|
- aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
|
|
|
- />
|
|
|
- </TooltipV2>
|
|
|
- </div>
|
|
|
- <div
|
|
|
- data-slot="bash-scroll"
|
|
|
- data-scrollable
|
|
|
- tabIndex={0}
|
|
|
- role="region"
|
|
|
- aria-label={i18n.t("ui.scrollView.ariaLabel")}
|
|
|
- >
|
|
|
- <pre data-slot="bash-pre">
|
|
|
- <code>
|
|
|
- <span data-slot="bash-prompt" aria-hidden="true">
|
|
|
- {"$ "}
|
|
|
- </span>
|
|
|
- {text()}
|
|
|
- </code>
|
|
|
- </pre>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <ConsoleOutput copy={command()}>
|
|
|
+ <span data-slot="bash-prompt" aria-hidden="true">
|
|
|
+ {"$ "}
|
|
|
+ </span>
|
|
|
+ {text()}
|
|
|
+ </ConsoleOutput>
|
|
|
</BasicTool>
|
|
|
)
|
|
|
},
|