feat(tui): add composer tabs
This commit is contained in:
@@ -626,6 +626,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
async remove(id: string) {
|
||||||
|
await sdk.client.v2.shell.remove({ id }, { throwOnError: true })
|
||||||
|
setStore("shell", id, undefined!)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
default() {
|
default() {
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import { createEffect, createMemo, For, onCleanup, Show, useContext, createContext } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { TextAttributes } from "@opentui/core"
|
||||||
|
import { useTheme } from "../../../context/theme"
|
||||||
|
import { SplitBorder } from "../../../ui/border"
|
||||||
|
import { useBindings, useOpencodeModeStack, useCommandShortcut } from "../../../keymap"
|
||||||
|
import { SubagentsTab } from "./subagents-tab"
|
||||||
|
import { ShellTab } from "./shell-tab"
|
||||||
|
|
||||||
|
export interface ComposerHint {
|
||||||
|
label: string
|
||||||
|
shortcut: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Tab {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
hints?: () => ComposerHint[]
|
||||||
|
onClose?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ComposerContext = createContext<{
|
||||||
|
register: (tab: Tab) => () => void
|
||||||
|
active: (id: string) => boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
export function useComposerTab() {
|
||||||
|
const ctx = useContext(ComposerContext)
|
||||||
|
if (!ctx) throw new Error("useComposerTab must be used within a Composer")
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComposerProps = {
|
||||||
|
sessionID: string
|
||||||
|
open: boolean
|
||||||
|
defaultTab?: string
|
||||||
|
onClose?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Composer(props: ComposerProps) {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
tabs: {} as Record<string, Tab>,
|
||||||
|
active: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
const tabList = createMemo(() => Object.values(store.tabs))
|
||||||
|
const activeTab = createMemo(() => tabList().find((t) => t.id === store.active))
|
||||||
|
const footerHints = createMemo(() => activeTab()?.hints?.() ?? [])
|
||||||
|
|
||||||
|
// Set active tab when opened
|
||||||
|
createEffect(() => {
|
||||||
|
if (!props.open) return
|
||||||
|
const tabs = tabList()
|
||||||
|
if (tabs.length === 0) return
|
||||||
|
const match = props.defaultTab && tabs.find((t) => t.id === props.defaultTab)
|
||||||
|
setStore("active", match ? match.id : tabs[0].id)
|
||||||
|
})
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
const tab = activeTab()
|
||||||
|
tab?.onClose?.()
|
||||||
|
props.onClose?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
register(tab: Tab) {
|
||||||
|
setStore("tabs", tab.id, tab)
|
||||||
|
if (!store.active) setStore("active", tab.id)
|
||||||
|
return () => setStore("tabs", tab.id, undefined!)
|
||||||
|
},
|
||||||
|
active(id: string) {
|
||||||
|
return props.open && store.active === id
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const modeStack = useOpencodeModeStack()
|
||||||
|
createEffect(() => {
|
||||||
|
if (!props.open) return
|
||||||
|
const popMode = modeStack.push("composer")
|
||||||
|
onCleanup(popMode)
|
||||||
|
})
|
||||||
|
|
||||||
|
const switchTab = (dir: number) => {
|
||||||
|
const tabs = tabList()
|
||||||
|
if (tabs.length <= 1) return
|
||||||
|
const idx = tabs.findIndex((t) => t.id === store.active)
|
||||||
|
setStore("active", tabs[(idx + dir + tabs.length) % tabs.length].id)
|
||||||
|
}
|
||||||
|
|
||||||
|
useBindings(() => ({
|
||||||
|
mode: "composer",
|
||||||
|
enabled: () => props.open,
|
||||||
|
bindings: [
|
||||||
|
{ key: "left", desc: "Previous tab", group: "Composer", cmd: () => switchTab(-1) },
|
||||||
|
{ key: "right", desc: "Next tab", group: "Composer", cmd: () => switchTab(1) },
|
||||||
|
{ key: "escape", desc: "Close composer", group: "Composer", cmd: close },
|
||||||
|
{
|
||||||
|
key: "<leader>down",
|
||||||
|
desc: "Toggle composer",
|
||||||
|
group: "Composer",
|
||||||
|
cmd: close,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
|
||||||
|
const closeHint = useCommandShortcut("session.child_first")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ComposerContext.Provider value={ctx}>
|
||||||
|
<box flexShrink={0} visible={props.open}>
|
||||||
|
<box
|
||||||
|
{...SplitBorder}
|
||||||
|
border={["left"]}
|
||||||
|
borderColor={theme.border}
|
||||||
|
backgroundColor={theme.backgroundPanel}
|
||||||
|
paddingLeft={1}
|
||||||
|
paddingRight={2}
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
>
|
||||||
|
<box gap={1}>
|
||||||
|
<Show
|
||||||
|
when={tabList().length > 1}
|
||||||
|
fallback={
|
||||||
|
<box flexDirection="row" paddingLeft={1}>
|
||||||
|
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
||||||
|
{tabList()[0]?.label ?? ""}
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<box flexDirection="row" gap={2} paddingLeft={1}>
|
||||||
|
<For each={tabList()}>
|
||||||
|
{(t) => {
|
||||||
|
const isActive = createMemo(() => store.active === t.id)
|
||||||
|
return (
|
||||||
|
<text
|
||||||
|
fg={isActive() ? theme.text : theme.textMuted}
|
||||||
|
attributes={isActive() ? TextAttributes.BOLD : undefined}
|
||||||
|
>
|
||||||
|
{t.label}
|
||||||
|
</text>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
<SubagentsTab sessionID={props.sessionID} />
|
||||||
|
<ShellTab sessionID={props.sessionID} />
|
||||||
|
<box flexDirection="row" gap={2} paddingLeft={1} flexShrink={0}>
|
||||||
|
<For each={footerHints()}>
|
||||||
|
{(hint) => (
|
||||||
|
<text>
|
||||||
|
<span style={{ fg: theme.text }}>
|
||||||
|
<b>{hint.label}</b>{" "}
|
||||||
|
</span>
|
||||||
|
<span style={{ fg: theme.textMuted }}>{hint.shortcut}</span>
|
||||||
|
</text>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<Show when={tabList().length > 1}>
|
||||||
|
<text>
|
||||||
|
<span style={{ fg: theme.text }}>
|
||||||
|
<b>tabs</b>{" "}
|
||||||
|
</span>
|
||||||
|
<span style={{ fg: theme.textMuted }}>←/→</span>
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
|
<text>
|
||||||
|
<span style={{ fg: theme.text }}>
|
||||||
|
<b>close</b>{" "}
|
||||||
|
</span>
|
||||||
|
<span style={{ fg: theme.textMuted }}>{closeHint()}</span>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
</ComposerContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { createMemo, For, Show, createEffect, onMount, onCleanup } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { TextAttributes, RGBA, ScrollBoxRenderable } from "@opentui/core"
|
||||||
|
import { useData } from "../../../context/data"
|
||||||
|
import { useTheme, selectedForeground } from "../../../context/theme"
|
||||||
|
import { useBindings, useCommandShortcut } from "../../../keymap"
|
||||||
|
import { useComposerTab } from "./index"
|
||||||
|
|
||||||
|
export function ShellTab(props: { sessionID: string }) {
|
||||||
|
const data = useData()
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const fg = selectedForeground(theme)
|
||||||
|
const composer = useComposerTab()
|
||||||
|
const killHint = useCommandShortcut("composer.shell.kill")
|
||||||
|
const backgroundHint = useCommandShortcut("composer.background")
|
||||||
|
|
||||||
|
const entries = createMemo(() =>
|
||||||
|
data.shell
|
||||||
|
.list()
|
||||||
|
.filter((shell) => shell.metadata.sessionID === props.sessionID && shell.status === "running"),
|
||||||
|
)
|
||||||
|
|
||||||
|
const [store, setStore] = createStore({ selected: 0 })
|
||||||
|
let scroll: ScrollBoxRenderable | undefined
|
||||||
|
|
||||||
|
const selectedEntry = createMemo(() => entries()[store.selected])
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (store.selected >= entries().length) setStore("selected", Math.max(0, entries().length - 1))
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!scroll) return
|
||||||
|
const target = scroll.getChildren()[store.selected]
|
||||||
|
if (!target) return
|
||||||
|
const y = target.y - scroll.y
|
||||||
|
if (y >= scroll.height || y < 0) {
|
||||||
|
const center = Math.floor(scroll.height / 2)
|
||||||
|
scroll.scrollBy(y - center)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const cleanup = composer.register({
|
||||||
|
id: "shell",
|
||||||
|
label: "Shell",
|
||||||
|
hints: () =>
|
||||||
|
selectedEntry()
|
||||||
|
? [
|
||||||
|
{ label: "kill", shortcut: killHint() },
|
||||||
|
{ label: "background", shortcut: backgroundHint() },
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
})
|
||||||
|
onCleanup(cleanup)
|
||||||
|
})
|
||||||
|
|
||||||
|
useBindings(() => ({
|
||||||
|
mode: "composer",
|
||||||
|
enabled: () => composer.active("shell"),
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
name: "composer.shell.up",
|
||||||
|
title: "Previous shell",
|
||||||
|
category: "Composer",
|
||||||
|
run() {
|
||||||
|
const list = entries()
|
||||||
|
if (list.length === 0) return
|
||||||
|
setStore("selected", (prev) => (prev - 1 + list.length) % list.length)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "composer.shell.down",
|
||||||
|
title: "Next shell",
|
||||||
|
category: "Composer",
|
||||||
|
run() {
|
||||||
|
const list = entries()
|
||||||
|
if (list.length === 0) return
|
||||||
|
setStore("selected", (prev) => (prev + 1) % list.length)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "composer.shell.kill",
|
||||||
|
title: "Kill shell command",
|
||||||
|
category: "Composer",
|
||||||
|
run() {
|
||||||
|
const entry = selectedEntry()
|
||||||
|
if (!entry) return
|
||||||
|
void data.shell.remove(entry.id)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "composer.background",
|
||||||
|
title: "Background shell command",
|
||||||
|
category: "Composer",
|
||||||
|
run() {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bindings: [
|
||||||
|
{ key: "up", desc: "Previous shell", group: "Shell", cmd: "composer.shell.up" },
|
||||||
|
{ key: "down", desc: "Next shell", group: "Shell", cmd: "composer.shell.down" },
|
||||||
|
{ key: "ctrl+d", desc: "Kill shell command", group: "Shell", cmd: "composer.shell.kill" },
|
||||||
|
{ key: "ctrl+b", desc: "Background shell command", group: "Shell", cmd: "composer.background" },
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={composer.active("shell")}>
|
||||||
|
<scrollbox
|
||||||
|
scrollbarOptions={{ visible: false }}
|
||||||
|
maxHeight={5}
|
||||||
|
ref={(r: ScrollBoxRenderable) => (scroll = r)}
|
||||||
|
>
|
||||||
|
<Show when={entries().length > 0} fallback={<text fg={theme.textMuted}> No shell commands</text>}>
|
||||||
|
<For each={entries()}>
|
||||||
|
{(shell, index) => {
|
||||||
|
const active = createMemo(() => index() === store.selected)
|
||||||
|
return (
|
||||||
|
<box
|
||||||
|
flexDirection="row"
|
||||||
|
paddingLeft={1}
|
||||||
|
paddingRight={1}
|
||||||
|
backgroundColor={active() ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
|
||||||
|
onMouseOver={() => setStore("selected", index())}
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
fg={active() ? fg : theme.text}
|
||||||
|
attributes={active() ? TextAttributes.BOLD : undefined}
|
||||||
|
wrapMode="none"
|
||||||
|
>
|
||||||
|
{shell.command}
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</scrollbox>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
import { createMemo, For, Show, createEffect, onMount, onCleanup } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { TextAttributes, RGBA, ScrollBoxRenderable } from "@opentui/core"
|
||||||
|
import { useRoute, useRouteData } from "../../../context/route"
|
||||||
|
import { useData } from "../../../context/data"
|
||||||
|
import { useTheme, selectedForeground } from "../../../context/theme"
|
||||||
|
import { Locale } from "../../../util/locale"
|
||||||
|
import { useBindings, useCommandShortcut } from "../../../keymap"
|
||||||
|
import { useComposerTab } from "./index"
|
||||||
|
|
||||||
|
interface SubagentEntry {
|
||||||
|
sessionID: string
|
||||||
|
agent: string
|
||||||
|
title: string
|
||||||
|
status: string
|
||||||
|
current: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SubagentsTab(props: { sessionID: string }) {
|
||||||
|
const route = useRouteData("session")
|
||||||
|
const data = useData()
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const fg = selectedForeground(theme)
|
||||||
|
const navigate = useRoute().navigate
|
||||||
|
const composer = useComposerTab()
|
||||||
|
const interruptHint = useCommandShortcut("composer.subagent.interrupt")
|
||||||
|
const backgroundHint = useCommandShortcut("composer.background")
|
||||||
|
|
||||||
|
const session = createMemo(() => data.session.get(props.sessionID))
|
||||||
|
|
||||||
|
const entries = createMemo<SubagentEntry[]>(() => {
|
||||||
|
const current = session()
|
||||||
|
if (!current) return []
|
||||||
|
|
||||||
|
const result: SubagentEntry[] = []
|
||||||
|
|
||||||
|
if (current.parentID) {
|
||||||
|
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 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),
|
||||||
|
current: sibling.id === route.sessionID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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 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),
|
||||||
|
current: child.id === route.sessionID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
const [store, setStore] = createStore({ selected: 0 })
|
||||||
|
let selectedSessionID = ""
|
||||||
|
let wasActive = false
|
||||||
|
let scroll: ScrollBoxRenderable | undefined
|
||||||
|
|
||||||
|
const selected = createMemo(() => {
|
||||||
|
return store.selected
|
||||||
|
})
|
||||||
|
const selectedEntry = createMemo(() => entries()[selected()])
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const active = composer.active("subagents")
|
||||||
|
if (!active) {
|
||||||
|
if (wasActive) {
|
||||||
|
selectedSessionID = ""
|
||||||
|
setStore("selected", 0)
|
||||||
|
}
|
||||||
|
wasActive = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const list = entries()
|
||||||
|
if (selectedSessionID !== route.sessionID && list.length > 0) {
|
||||||
|
const currentIdx = list.findIndex((e) => e.current)
|
||||||
|
const next = currentIdx >= 0 ? currentIdx : 0
|
||||||
|
selectedSessionID = route.sessionID
|
||||||
|
setStore("selected", next)
|
||||||
|
const scrollCurrentIntoView = () => scrollToIndex(next, true)
|
||||||
|
scrollCurrentIntoView()
|
||||||
|
requestAnimationFrame(scrollCurrentIntoView)
|
||||||
|
}
|
||||||
|
wasActive = true
|
||||||
|
if (store.selected >= list.length) moveTo(Math.max(0, list.length - 1))
|
||||||
|
})
|
||||||
|
|
||||||
|
function moveTo(next: number, center = false) {
|
||||||
|
setStore("selected", next)
|
||||||
|
scrollToSelection(center)
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToSelection(center: boolean) {
|
||||||
|
scrollToIndex(selected(), center)
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToIndex(index: number, center: boolean) {
|
||||||
|
if (!scroll) return
|
||||||
|
if (center) {
|
||||||
|
scroll.scrollTo(Math.max(0, index - Math.floor(scroll.viewport.height / 2)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (index >= scroll.scrollTop + scroll.viewport.height) {
|
||||||
|
scroll.scrollTo(index - scroll.viewport.height + 1)
|
||||||
|
}
|
||||||
|
if (index < scroll.scrollTop) {
|
||||||
|
scroll.scrollTo(index)
|
||||||
|
if (index === 0) scroll.scrollTo(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const cleanup = composer.register({
|
||||||
|
id: "subagents",
|
||||||
|
label: "Subagents",
|
||||||
|
hints: () => {
|
||||||
|
const entry = selectedEntry()
|
||||||
|
if (!entry || entry.status !== "running") return []
|
||||||
|
return [
|
||||||
|
{ label: "interrupt", shortcut: interruptHint() },
|
||||||
|
...(entry.current ? [{ label: "background", shortcut: backgroundHint() }] : []),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
onClose: () => {
|
||||||
|
const parentID = session()?.parentID
|
||||||
|
if (parentID) navigate({ type: "session", sessionID: parentID })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
onCleanup(cleanup)
|
||||||
|
})
|
||||||
|
|
||||||
|
useBindings(() => ({
|
||||||
|
mode: "composer",
|
||||||
|
enabled: () => composer.active("subagents"),
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
name: "composer.subagent.up",
|
||||||
|
title: "Previous subagent",
|
||||||
|
category: "Composer",
|
||||||
|
run() {
|
||||||
|
const list = entries()
|
||||||
|
if (list.length === 0) return
|
||||||
|
moveTo((store.selected - 1 + list.length) % list.length, true)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "composer.subagent.down",
|
||||||
|
title: "Next subagent",
|
||||||
|
category: "Composer",
|
||||||
|
run() {
|
||||||
|
const list = entries()
|
||||||
|
if (list.length === 0) return
|
||||||
|
moveTo((store.selected + 1) % list.length, true)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "composer.subagent.select",
|
||||||
|
title: "Navigate to subagent",
|
||||||
|
category: "Composer",
|
||||||
|
run() {
|
||||||
|
const entry = entries()[store.selected]
|
||||||
|
if (entry) navigate({ type: "session", sessionID: entry.sessionID })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "composer.subagent.interrupt",
|
||||||
|
title: "Interrupt subagent",
|
||||||
|
category: "Composer",
|
||||||
|
run() {
|
||||||
|
const entry = selectedEntry()
|
||||||
|
if (!entry || entry.status !== "running") return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "composer.background",
|
||||||
|
title: "Background subagent",
|
||||||
|
category: "Composer",
|
||||||
|
run() {
|
||||||
|
const entry = selectedEntry()
|
||||||
|
if (!entry || entry.status !== "running" || !entry.current) return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bindings: [
|
||||||
|
{ 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+d", desc: "Interrupt subagent", group: "Subagents", cmd: "composer.subagent.interrupt" },
|
||||||
|
{ key: "ctrl+b", desc: "Background subagent", group: "Subagents", cmd: "composer.background" },
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={composer.active("subagents")}>
|
||||||
|
<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"
|
||||||
|
paddingLeft={1}
|
||||||
|
paddingRight={1}
|
||||||
|
backgroundColor={active() ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
|
||||||
|
onMouseOver={() => setStore("selected", index())}
|
||||||
|
onMouseUp={() => {
|
||||||
|
setStore("selected", index())
|
||||||
|
navigate({ type: "session", sessionID: entry.sessionID })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<box flexGrow={1} minWidth={0} flexDirection="row">
|
||||||
|
<text
|
||||||
|
fg={active() ? fg : entry.current ? theme.primary : theme.text}
|
||||||
|
attributes={active() ? TextAttributes.BOLD : undefined}
|
||||||
|
wrapMode="none"
|
||||||
|
>
|
||||||
|
{entry.agent}: {entry.title}
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
<Show when={status()}>
|
||||||
|
<text fg={active() ? fg : theme.textMuted} wrapMode="none">
|
||||||
|
{status()}
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</scrollbox>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user