Просмотр исходного кода

fix(tui): improve subagent picker states

Kit Langton 1 месяц назад
Родитель
Сommit
57c2c78451
1 измененных файлов с 60 добавлено и 15 удалено
  1. 60 15
      packages/tui/src/routes/session/composer/subagents-tab.tsx

+ 60 - 15
packages/tui/src/routes/session/composer/subagents-tab.tsx

@@ -8,12 +8,15 @@ import { useTheme, selectedForeground } from "../../../context/theme"
 import { Locale } from "../../../util/locale"
 import { useBindings, useCommandShortcut } from "../../../keymap"
 import { useComposerTab } from "./index"
+import { Spinner } from "../../../component/spinner"
+import type { SessionMessageInfo } from "@opencode-ai/client"
 
 interface SubagentEntry {
   sessionID: string
   agent: string
   title: string
   status: string
+  background: boolean
   current: boolean
 }
 
@@ -26,6 +29,7 @@ export function SubagentsTab(props: { sessionID: string }) {
   const navigate = useRoute().navigate
   const composer = useComposerTab()
   const interruptHint = useCommandShortcut("composer.subagent.interrupt")
+  const backgroundHint = useCommandShortcut("composer.subagent.background")
 
   const session = createMemo(() => data.session.get(props.sessionID))
 
@@ -39,13 +43,18 @@ export function SubagentsTab(props: { sessionID: string }) {
       const siblings = data.session.list().filter((s) => s.parentID === current.parentID)
       for (const sibling of siblings) {
         const agentMatch = sibling.title.match(/@(\w+) subagent/)
-        const agent = sibling.agent ? Locale.titlecase(sibling.agent) : agentMatch ? Locale.titlecase(agentMatch[1]) : "Subagent"
+        const agent = sibling.agent
+          ? Locale.titlecase(sibling.agent)
+          : agentMatch
+            ? Locale.titlecase(agentMatch[1])
+            : "Subagent"
         const name = agentMatch ? sibling.title.replace(agentMatch[0], "").trim() || sibling.title : sibling.title
         result.push({
           sessionID: sibling.id,
           agent,
           title: name,
           status: data.session.status(sibling.id),
+          background: isBackgroundSubagent(data.session.message.list(current.parentID), sibling.id, sibling.title),
           current: sibling.id === route.sessionID,
         })
       }
@@ -53,13 +62,18 @@ export function SubagentsTab(props: { sessionID: string }) {
       const children = data.session.list().filter((s) => s.parentID === props.sessionID)
       for (const child of children) {
         const agentMatch = child.title.match(/@(\w+) subagent/)
-        const agent = child.agent ? Locale.titlecase(child.agent) : agentMatch ? Locale.titlecase(agentMatch[1]) : "Subagent"
+        const agent = child.agent
+          ? Locale.titlecase(child.agent)
+          : agentMatch
+            ? Locale.titlecase(agentMatch[1])
+            : "Subagent"
         const name = agentMatch ? child.title.replace(agentMatch[0], "").trim() || child.title : child.title
         result.push({
           sessionID: child.id,
           agent,
           title: name,
           status: data.session.status(child.id),
+          background: isBackgroundSubagent(data.session.message.list(props.sessionID), child.id, child.title),
           current: child.id === route.sessionID,
         })
       }
@@ -133,7 +147,10 @@ export function SubagentsTab(props: { sessionID: string }) {
       hints: () => {
         const entry = selectedEntry()
         if (!entry || entry.status !== "running") return []
-        return [{ label: "interrupt", shortcut: interruptHint() }]
+        return [
+          ...(!entry.background && backgroundHint() ? [{ label: "background", shortcut: backgroundHint() }] : []),
+          { label: "interrupt", shortcut: interruptHint() },
+        ]
       },
       onClose: () => {
         const parentID = session()?.parentID
@@ -176,6 +193,16 @@ export function SubagentsTab(props: { sessionID: string }) {
           if (entry) navigate({ type: "session", sessionID: entry.sessionID })
         },
       },
+      {
+        name: "composer.subagent.background",
+        title: "Background blocking tools",
+        category: "Session",
+        run() {
+          const entry = selectedEntry()
+          if (!entry || entry.status !== "running" || entry.background) return
+          void sdk.api.session.background({ sessionID: session()?.parentID ?? props.sessionID })
+        },
+      },
       {
         name: "composer.subagent.interrupt",
         title: "Interrupt subagent",
@@ -191,25 +218,18 @@ export function SubagentsTab(props: { sessionID: string }) {
       { key: "up", desc: "Previous subagent", group: "Subagents", cmd: "composer.subagent.up" },
       { key: "down", desc: "Next subagent", group: "Subagents", cmd: "composer.subagent.down" },
       { key: "return", desc: "Navigate to subagent", group: "Subagents", cmd: "composer.subagent.select" },
+      { key: "ctrl+b", desc: "Background subagent", group: "Subagents", cmd: "composer.subagent.background" },
       { key: "ctrl+d", desc: "Interrupt subagent", group: "Subagents", cmd: "composer.subagent.interrupt" },
     ],
   }))
 
   return (
     <Show when={composer.active("subagents")}>
-      <scrollbox
-        scrollbarOptions={{ visible: false }}
-        maxHeight={5}
-        ref={(r: ScrollBoxRenderable) => (scroll = r)}
-      >
+      <scrollbox scrollbarOptions={{ visible: false }} maxHeight={5} ref={(r: ScrollBoxRenderable) => (scroll = r)}>
         <Show when={entries().length > 0} fallback={<text fg={theme.textMuted}> No subagents</text>}>
           <For each={entries()}>
             {(entry, index) => {
               const active = createMemo(() => index() === selected())
-              const status = createMemo(() => {
-                if (entry.status === "running") return "Running"
-                return ""
-              })
               return (
                 <box
                   flexDirection="row"
@@ -222,18 +242,30 @@ export function SubagentsTab(props: { sessionID: string }) {
                     navigate({ type: "session", sessionID: entry.sessionID })
                   }}
                 >
+                  <box width={2} flexShrink={0}>
+                    <Show when={entry.status === "running" && !entry.background}>
+                      <Spinner color={active() ? fg : theme.textMuted} />
+                    </Show>
+                  </box>
                   <box flexGrow={1} minWidth={0} flexDirection="row">
+                    <text
+                      fg={active() ? fg : theme.textMuted}
+                      attributes={active() ? TextAttributes.BOLD : undefined}
+                      wrapMode="none"
+                    >
+                      {entry.agent}{" "}
+                    </text>
                     <text
                       fg={active() ? fg : entry.current ? theme.primary : theme.text}
                       attributes={active() ? TextAttributes.BOLD : undefined}
                       wrapMode="none"
                     >
-                      {entry.agent}: {entry.title}
+                      {entry.title}
                     </text>
                   </box>
-                  <Show when={status()}>
+                  <Show when={entry.status === "running" && entry.background}>
                     <text fg={active() ? fg : theme.textMuted} wrapMode="none">
-                      {status()}
+                      background
                     </text>
                   </Show>
                 </box>
@@ -245,3 +277,16 @@ export function SubagentsTab(props: { sessionID: string }) {
     </Show>
   )
 }
+
+function isBackgroundSubagent(messages: SessionMessageInfo[], sessionID: string, title: string) {
+  return messages.some((message) =>
+    message.type === "assistant"
+      ? message.content.some((part) => {
+          if (part.type !== "tool" || part.name !== "subagent" || part.state.status === "streaming") return false
+          if (part.state.input.background === true && part.state.input.description === title) return true
+          if (part.state.status === "running") return false
+          return part.state.structured.sessionID === sessionID && part.state.structured.status === "running"
+        })
+      : false,
+  )
+}