|
|
@@ -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,
|
|
|
+ )
|
|
|
+}
|