Explorar o código

fix(app): task error state

Adam hai 1 mes
pai
achega
9d3c42c8c4

+ 3 - 3
packages/opencode/src/session/prompt.ts

@@ -425,14 +425,14 @@ export namespace SessionPrompt {
           extra: { bypassAgentCheck: true },
           messages: msgs,
           async metadata(input) {
-            await Session.updatePart({
+            part = (await Session.updatePart({
               ...part,
               type: "tool",
               state: {
                 ...part.state,
                 ...input,
               },
-            } satisfies MessageV2.ToolPart)
+            } satisfies MessageV2.ToolPart)) as MessageV2.ToolPart
           },
           async ask(req) {
             await PermissionNext.ask({
@@ -493,7 +493,7 @@ export namespace SessionPrompt {
                 start: part.state.status === "running" ? part.state.time.start : Date.now(),
                 end: Date.now(),
               },
-              metadata: part.metadata,
+              metadata: "metadata" in part.state ? part.state.metadata : undefined,
               input: part.state.input,
             },
           } satisfies MessageV2.ToolPart)

+ 43 - 19
packages/ui/src/components/message-part.tsx

@@ -344,6 +344,17 @@ function urls(text: string | undefined) {
     })
 }
 
+function sessionLink(id: string | undefined, path: string, href?: (id: string) => string | undefined) {
+  if (!id) return
+
+  const direct = href?.(id)
+  if (direct) return direct
+
+  const idx = path.indexOf("/session")
+  if (idx === -1) return
+  return `${path.slice(0, idx)}/session/${id}`
+}
+
 const CONTEXT_GROUP_TOOLS = new Set(["read", "glob", "grep", "list"])
 const HIDDEN_TOOLS = new Set(["todowrite", "todoread"])
 
@@ -1215,6 +1226,7 @@ function ToolFileAccordion(props: { path: string; actions?: JSX.Element; childre
 }
 
 PART_MAPPING["tool"] = function ToolPartDisplay(props) {
+  const data = useData()
   const i18n = useI18n()
   const part = () => props.part as ToolPart
   if (part().tool === "todowrite" || part().tool === "todoread") return null
@@ -1229,6 +1241,21 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
   const input = () => part().state?.input ?? emptyInput
   // @ts-expect-error
   const partMetadata = () => part().state?.metadata ?? emptyMetadata
+  const taskId = createMemo(() => {
+    if (part().tool !== "task") return
+    const value = partMetadata().sessionId
+    if (typeof value === "string" && value) return value
+  })
+  const taskHref = createMemo(() => {
+    if (part().tool !== "task") return
+    return sessionLink(taskId(), useLocation().pathname, data.sessionHref)
+  })
+  const taskSubtitle = createMemo(() => {
+    if (part().tool !== "task") return undefined
+    const value = input().description
+    if (typeof value === "string" && value) return value
+    return taskId()
+  })
 
   const render = createMemo(() => ToolRegistry.render(part().tool) ?? GenericTool)
 
@@ -1248,7 +1275,15 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
                   </div>
                 )
               }
-              return <ToolErrorCard tool={part().tool} error={error()} defaultOpen={props.defaultOpen} />
+              return (
+                <ToolErrorCard
+                  tool={part().tool}
+                  error={error()}
+                  defaultOpen={props.defaultOpen}
+                  subtitle={taskSubtitle()}
+                  href={taskHref()}
+                />
+              )
             }}
           </Match>
           <Match when={true}>
@@ -1625,25 +1660,14 @@ ToolRegistry.register({
       return raw[0]!.toUpperCase() + raw.slice(1)
     })
     const title = createMemo(() => agentTitle(i18n, type()))
-    const description = createMemo(() => {
+    const subtitle = createMemo(() => {
       const value = props.input.description
-      if (typeof value === "string") return value
-      return undefined
+      if (typeof value === "string" && value) return value
+      return childSessionId()
     })
     const running = createMemo(() => props.status === "pending" || props.status === "running")
 
-    const href = createMemo(() => {
-      const sessionId = childSessionId()
-      if (!sessionId) return
-
-      const direct = data.sessionHref?.(sessionId)
-      if (direct) return direct
-
-      const path = location.pathname
-      const idx = path.indexOf("/session")
-      if (idx === -1) return
-      return `${path.slice(0, idx)}/session/${sessionId}`
-    })
+    const href = createMemo(() => sessionLink(childSessionId(), location.pathname, data.sessionHref))
 
     const titleContent = () => <TextShimmer text={title()} active={running()} />
 
@@ -1653,7 +1677,7 @@ ToolRegistry.register({
           <span data-slot="basic-tool-tool-title" class="capitalize agent-title">
             {titleContent()}
           </span>
-          <Show when={description()}>
+          <Show when={subtitle()}>
             <Switch>
               <Match when={href()}>
                 <a
@@ -1662,11 +1686,11 @@ ToolRegistry.register({
                   href={href()!}
                   onClick={(e) => e.stopPropagation()}
                 >
-                  {description()}
+                  {subtitle()}
                 </a>
               </Match>
               <Match when={true}>
-                <span data-slot="basic-tool-tool-subtitle">{description()}</span>
+                <span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>
               </Match>
             </Switch>
           </Show>

+ 19 - 2
packages/ui/src/components/tool-error-card.tsx

@@ -10,19 +10,22 @@ export interface ToolErrorCardProps extends Omit<ComponentProps<typeof Card>, "c
   tool: string
   error: string
   defaultOpen?: boolean
+  subtitle?: string
+  href?: string
 }
 
 export function ToolErrorCard(props: ToolErrorCardProps) {
   const i18n = useI18n()
   const [open, setOpen] = createSignal(props.defaultOpen ?? false)
   const [copied, setCopied] = createSignal(false)
-  const [split, rest] = splitProps(props, ["tool", "error", "defaultOpen"])
+  const [split, rest] = splitProps(props, ["tool", "error", "defaultOpen", "subtitle", "href"])
   const name = createMemo(() => {
     const map: Record<string, string> = {
       read: "ui.tool.read",
       list: "ui.tool.list",
       glob: "ui.tool.glob",
       grep: "ui.tool.grep",
+      task: "Task",
       webfetch: "ui.tool.webfetch",
       websearch: "ui.tool.websearch",
       codesearch: "ui.tool.codesearch",
@@ -32,6 +35,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
     }
     const key = map[split.tool]
     if (!key) return split.tool
+    if (!key.includes(".")) return key
     return i18n.t(key)
   })
   const cleaned = createMemo(() => split.error.replace(/^Error:\s*/, "").trim())
@@ -43,6 +47,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
   })
 
   const subtitle = createMemo(() => {
+    if (split.subtitle) return split.subtitle
     const parts = tail().split(": ")
     if (parts.length <= 1) return "Failed"
     const head = (parts[0] ?? "").trim()
@@ -77,7 +82,19 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
                 <div data-slot="basic-tool-tool-info-structured">
                   <div data-slot="basic-tool-tool-info-main">
                     <span data-slot="basic-tool-tool-title">{name()}</span>
-                    <span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>
+                    <Show
+                      when={split.href && split.subtitle}
+                      fallback={<span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>}
+                    >
+                      <a
+                        data-slot="basic-tool-tool-subtitle"
+                        class="clickable subagent-link"
+                        href={split.href!}
+                        onClick={(e) => e.stopPropagation()}
+                      >
+                        {subtitle()}
+                      </a>
+                    </Show>
                   </div>
                 </div>
               </div>