瀏覽代碼

fix(tui): show background subagent completion (#36530)

Co-authored-by: Kit Langton <7587245+kitlangton@users.noreply.github.com>
Kit Langton 4 周之前
父節點
當前提交
5414697bd1

+ 16 - 5
packages/core/src/tool/subagent.ts

@@ -65,6 +65,7 @@ export const Plugin = {
     const injectCompletion = Effect.fn("SubagentTool.injectCompletion")(function* (
       parentID: SessionSchema.ID,
       childID: SessionSchema.ID,
+      agent: string,
       description: string,
       state: "completed" | "error" | "cancelled",
       text: string,
@@ -72,22 +73,32 @@ export const Plugin = {
       yield* runtime.session.synthetic({
         sessionID: parentID,
         text: `<subagent id="${childID}" state="${state}" description="${description}">\n${text}\n</subagent>`,
+        description,
+        metadata: { source: "subagent", childID, agent, state },
       })
     })
 
     const notifyWhenDone = Effect.fn("SubagentTool.notifyWhenDone")(function* (
       parentID: SessionSchema.ID,
       childID: SessionSchema.ID,
+      agent: string,
       description: string,
     ) {
       yield* runtime.job.wait({ id: childID }).pipe(
         Effect.flatMap((result) => {
           if (result.info?.status === "completed")
-            return injectCompletion(parentID, childID, description, "completed", result.info.output ?? NO_TEXT)
+            return injectCompletion(parentID, childID, agent, description, "completed", result.info.output ?? NO_TEXT)
           if (result.info?.status === "error")
-            return injectCompletion(parentID, childID, description, "error", result.info.error ?? "Subagent failed")
+            return injectCompletion(
+              parentID,
+              childID,
+              agent,
+              description,
+              "error",
+              result.info.error ?? "Subagent failed",
+            )
           if (result.info?.status === "cancelled")
-            return injectCompletion(parentID, childID, description, "cancelled", "Subagent cancelled")
+            return injectCompletion(parentID, childID, agent, description, "cancelled", "Subagent cancelled")
           return Effect.void
         }),
         Effect.forkIn(scope, { startImmediately: true }),
@@ -167,7 +178,7 @@ export const Plugin = {
 
                 if (background) {
                   yield* runtime.job.background(info.id)
-                  yield* notifyWhenDone(context.sessionID, child.id, input.description)
+                  yield* notifyWhenDone(context.sessionID, child.id, agent.name, input.description)
                   return {
                     sessionID: child.id,
                     status: "running" as const,
@@ -183,7 +194,7 @@ export const Plugin = {
                   ),
                 )
                 if (result?.type === "backgrounded") {
-                  yield* notifyWhenDone(context.sessionID, child.id, input.description)
+                  yield* notifyWhenDone(context.sessionID, child.id, agent.name, input.description)
                   return {
                     sessionID: child.id,
                     status: "running" as const,

+ 9 - 0
packages/core/test/tool-subagent.test.ts

@@ -288,6 +288,15 @@ describe("SubagentTool", () => {
 
           const admission = Array.from(yield* Fiber.join(admitted))[0]
           expect(admission?.data.input.data.text).toContain(`<subagent id="${childID}" state="completed"`)
+          expect(admission?.data.input.data).toMatchObject({
+            description: "background review",
+            metadata: {
+              source: "subagent",
+              childID,
+              agent: "reviewer",
+              state: "completed",
+            },
+          })
           const database = yield* Database.Service
           yield* SessionPending.promoteSteers(database.db, events, parent.id)
           const synthetic = (yield* sessions.context(parent.id)).filter((message) => message.type === "synthetic")

+ 31 - 3
packages/tui/src/routes/session/index.tsx

@@ -1245,15 +1245,43 @@ function SessionSwitchMessageV2(props: { message: SessionMessageInfo }) {
 
 function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
   const { theme } = useTheme()
+  const metadata = () => (props.message.type === "synthetic" ? props.message.metadata : undefined)
+  const completion = () => metadata()?.source === "subagent"
+  const state = () => stringValue(metadata()?.state)
+  const agent = () => Locale.titlecase(stringValue(metadata()?.agent) ?? "Subagent")
   const text = () => {
     if (props.message.type === "system") return props.message.text
     if (props.message.type === "synthetic") return props.message.description ?? ""
     return ""
   }
+  const status = () => {
+    if (state() === "completed") return "finished"
+    if (state() === "error") return "failed"
+    return state() ?? "finished"
+  }
+  const color = () => {
+    if (state() === "error") return theme.error
+    if (state() === "cancelled") return theme.warning
+    return theme.info
+  }
   return (
-    <InlineToolRow icon="◈" color={theme.textMuted} pending="Notice" complete={true}>
-      {text()}
-    </InlineToolRow>
+    <Show
+      when={completion()}
+      fallback={
+        <InlineToolRow icon="◈" color={theme.textMuted} pending="Notice" complete={true}>
+          {text()}
+        </InlineToolRow>
+      }
+    >
+      <box marginLeft={3}>
+        <text>
+          <span style={{ fg: color() }}>
+            {state() === "completed" ? "↳" : "!"} {agent()} {status()}
+          </span>
+          <span style={{ fg: theme.textMuted }}> · {text()}</span>
+        </text>
+      </box>
+    </Show>
   )
 }