Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton 57c2c78451 fix(tui): improve subagent picker states 2026-07-12 01:46:17 +00:00
@@ -8,12 +8,15 @@ import { useTheme, selectedForeground } from "../../../context/theme"
import { Locale } from "../../../util/locale" import { Locale } from "../../../util/locale"
import { useBindings, useCommandShortcut } from "../../../keymap" import { useBindings, useCommandShortcut } from "../../../keymap"
import { useComposerTab } from "./index" import { useComposerTab } from "./index"
import { Spinner } from "../../../component/spinner"
import type { SessionMessageInfo } from "@opencode-ai/client"
interface SubagentEntry { interface SubagentEntry {
sessionID: string sessionID: string
agent: string agent: string
title: string title: string
status: string status: string
background: boolean
current: boolean current: boolean
} }
@@ -26,6 +29,7 @@ export function SubagentsTab(props: { sessionID: string }) {
const navigate = useRoute().navigate const navigate = useRoute().navigate
const composer = useComposerTab() const composer = useComposerTab()
const interruptHint = useCommandShortcut("composer.subagent.interrupt") const interruptHint = useCommandShortcut("composer.subagent.interrupt")
const backgroundHint = useCommandShortcut("composer.subagent.background")
const session = createMemo(() => data.session.get(props.sessionID)) 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) const siblings = data.session.list().filter((s) => s.parentID === current.parentID)
for (const sibling of siblings) { for (const sibling of siblings) {
const agentMatch = sibling.title.match(/@(\w+) subagent/) 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 const name = agentMatch ? sibling.title.replace(agentMatch[0], "").trim() || sibling.title : sibling.title
result.push({ result.push({
sessionID: sibling.id, sessionID: sibling.id,
agent, agent,
title: name, title: name,
status: data.session.status(sibling.id), status: data.session.status(sibling.id),
background: isBackgroundSubagent(data.session.message.list(current.parentID), sibling.id, sibling.title),
current: sibling.id === route.sessionID, 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) const children = data.session.list().filter((s) => s.parentID === props.sessionID)
for (const child of children) { for (const child of children) {
const agentMatch = child.title.match(/@(\w+) subagent/) 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 const name = agentMatch ? child.title.replace(agentMatch[0], "").trim() || child.title : child.title
result.push({ result.push({
sessionID: child.id, sessionID: child.id,
agent, agent,
title: name, title: name,
status: data.session.status(child.id), status: data.session.status(child.id),
background: isBackgroundSubagent(data.session.message.list(props.sessionID), child.id, child.title),
current: child.id === route.sessionID, current: child.id === route.sessionID,
}) })
} }
@@ -133,7 +147,10 @@ export function SubagentsTab(props: { sessionID: string }) {
hints: () => { hints: () => {
const entry = selectedEntry() const entry = selectedEntry()
if (!entry || entry.status !== "running") return [] if (!entry || entry.status !== "running") return []
return [{ label: "interrupt", shortcut: interruptHint() }] return [
...(!entry.background && backgroundHint() ? [{ label: "background", shortcut: backgroundHint() }] : []),
{ label: "interrupt", shortcut: interruptHint() },
]
}, },
onClose: () => { onClose: () => {
const parentID = session()?.parentID const parentID = session()?.parentID
@@ -176,6 +193,16 @@ export function SubagentsTab(props: { sessionID: string }) {
if (entry) navigate({ type: "session", sessionID: entry.sessionID }) 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", name: "composer.subagent.interrupt",
title: "Interrupt subagent", 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: "up", desc: "Previous subagent", group: "Subagents", cmd: "composer.subagent.up" },
{ key: "down", desc: "Next subagent", group: "Subagents", cmd: "composer.subagent.down" }, { key: "down", desc: "Next subagent", group: "Subagents", cmd: "composer.subagent.down" },
{ key: "return", desc: "Navigate to subagent", group: "Subagents", cmd: "composer.subagent.select" }, { 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" }, { key: "ctrl+d", desc: "Interrupt subagent", group: "Subagents", cmd: "composer.subagent.interrupt" },
], ],
})) }))
return ( return (
<Show when={composer.active("subagents")}> <Show when={composer.active("subagents")}>
<scrollbox <scrollbox scrollbarOptions={{ visible: false }} maxHeight={5} ref={(r: ScrollBoxRenderable) => (scroll = r)}>
scrollbarOptions={{ visible: false }}
maxHeight={5}
ref={(r: ScrollBoxRenderable) => (scroll = r)}
>
<Show when={entries().length > 0} fallback={<text fg={theme.textMuted}> No subagents</text>}> <Show when={entries().length > 0} fallback={<text fg={theme.textMuted}> No subagents</text>}>
<For each={entries()}> <For each={entries()}>
{(entry, index) => { {(entry, index) => {
const active = createMemo(() => index() === selected()) const active = createMemo(() => index() === selected())
const status = createMemo(() => {
if (entry.status === "running") return "Running"
return ""
})
return ( return (
<box <box
flexDirection="row" flexDirection="row"
@@ -222,18 +242,30 @@ export function SubagentsTab(props: { sessionID: string }) {
navigate({ type: "session", sessionID: entry.sessionID }) 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"> <box flexGrow={1} minWidth={0} flexDirection="row">
<text
fg={active() ? fg : theme.textMuted}
attributes={active() ? TextAttributes.BOLD : undefined}
wrapMode="none"
>
{entry.agent}{" "}
</text>
<text <text
fg={active() ? fg : entry.current ? theme.primary : theme.text} fg={active() ? fg : entry.current ? theme.primary : theme.text}
attributes={active() ? TextAttributes.BOLD : undefined} attributes={active() ? TextAttributes.BOLD : undefined}
wrapMode="none" wrapMode="none"
> >
{entry.agent}: {entry.title} {entry.title}
</text> </text>
</box> </box>
<Show when={status()}> <Show when={entry.status === "running" && entry.background}>
<text fg={active() ? fg : theme.textMuted} wrapMode="none"> <text fg={active() ? fg : theme.textMuted} wrapMode="none">
{status()} background
</text> </text>
</Show> </Show>
</box> </box>
@@ -245,3 +277,16 @@ export function SubagentsTab(props: { sessionID: string }) {
</Show> </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,
)
}