diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 48e609a87f9..79f482bd6b3 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -1099,9 +1099,7 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi - - {(_) => } - + {plugin()} diff --git a/packages/tui/src/routes/session/child-session-picker.tsx b/packages/tui/src/routes/session/child-session-picker.tsx new file mode 100644 index 00000000000..9a61cc1848b --- /dev/null +++ b/packages/tui/src/routes/session/child-session-picker.tsx @@ -0,0 +1,114 @@ +import type { SessionV2Info } from "@opencode-ai/sdk/v2" +import { createSignal, For, onCleanup, onMount } from "solid-js" +import { useRenderer } from "@opentui/solid" +import { selectedForeground, useTheme } from "../../context/theme" +import { useBindings, useOpencodeModeStack } from "../../keymap" +import { SplitBorder } from "../../ui/border" + +const mode = "child-session-picker" + +export function ChildSessionPicker(props: { + sessions: SessionV2Info[] + currentID: string + child: boolean + status: (sessionID: string) => "idle" | "running" + onSelect: (sessionID: string) => void + onEscape: () => void +}) { + const renderer = useRenderer() + const modeStack = useOpencodeModeStack() + const { theme } = useTheme() + const [selected, setSelected] = createSignal( + Math.max( + 0, + props.sessions.findIndex((item) => item.id === props.currentID), + ), + ) + const move = (direction: -1 | 1) => + setSelected((selected() + direction + props.sessions.length) % props.sessions.length) + + onMount(() => { + const popMode = modeStack.push(mode) + onCleanup(popMode) + }) + + useBindings(() => ({ + mode, + bindings: [ + { key: "up", desc: "Previous child agent", group: "Child agents", cmd: () => move(-1) }, + { key: "down", desc: "Next child agent", group: "Child agents", cmd: () => move(1) }, + { + key: "return", + desc: "Open child agent", + group: "Child agents", + cmd: () => props.onSelect(props.sessions[selected()].id), + }, + { + key: "escape", + desc: props.child ? "Return to parent session" : "Close child agents", + group: "Child agents", + cmd: props.onEscape, + }, + ], + })) + + return ( + + + Child agents + + Choose an agent to view its session + + + {(session, index) => { + const active = () => selected() === index() + const running = () => props.status(session.id) === "running" + const foreground = () => (active() ? selectedForeground(theme, theme.accent) : theme.text) + return ( + setSelected(index())} + onMouseUp={() => { + if (renderer.getSelection()?.getSelectedText()) return + props.onSelect(session.id) + }} + > + + + {running() ? "●" : "○"} + + + @{session.agent ?? "agent"}{" "} + {session.title.replace(/\s+\(@[^)]+ subagent\)$/, "")} + + + + {running() ? "Running" : "Complete"} + + + ) + }} + + + + ↑↓ navigate · enter open + {props.child ? "esc parent" : "esc close"} + + + ) +} diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 517027cc155..4f2bea5481f 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -69,6 +69,7 @@ import { OPENCODE_BASE_MODE, useBindings } from "../../keymap" import { usePathFormatter } from "../../context/path-format" import { LocationProvider } from "../../context/location" import { createSessionRows, type PartRef, type SessionRow } from "./rows" +import { ChildSessionPicker } from "./child-session-picker" addDefaultParsers(parsers.parsers) @@ -137,6 +138,10 @@ function use() { } export function Session() { + const route = useRouteData("session") + const [childPicker, setChildPicker] = createSignal(false) + + function SessionView() { const setEpilogue = useEpilogue() const clipboard = useClipboard() const writeExport = async (file: string, content: string) => { @@ -175,8 +180,23 @@ export function Session() { if (session()?.parentID) return [] return data.session.question.list(route.sessionID) ?? [] }) - const visible = createMemo(() => !session()?.parentID && permissions().length === 0 && questions().length === 0) const disabled = createMemo(() => permissions().length > 0 || questions().length > 0) + const childSessions = createMemo(() => { + const parentID = session()?.parentID ?? session()?.id + if (!parentID) return [] + return data.session + .list() + .filter((item) => item.parentID === parentID) + .toSorted((a, b) => { + const running = + Number(data.session.status(b.id) === "running") - Number(data.session.status(a.id) === "running") + return running || b.time.updated - a.time.updated + }) + }) + const childPickerVisible = createMemo( + () => (!!session()?.parentID || childPicker()) && !disabled() && childSessions().length > 0, + ) + const visible = createMemo(() => !session()?.parentID && !childPickerVisible() && !disabled()) const pending = createMemo(() => { const completed = messages().findLast((x) => x.type === "assistant" && x.time.completed)?.id @@ -726,11 +746,15 @@ export function Session() { run: () => unavailable("Backgrounding subagents"), }, { - title: "Go to child session", + title: "View child agents", value: "session.child.first", category: "Session", hidden: true, - run: () => unavailable("Child session discovery"), + enabled: childSessions().length > 0 && !disabled(), + run: () => { + setChildPicker(true) + dialog.clear() + }, }, { title: "Go to parent session", @@ -863,7 +887,28 @@ export function Session() { directory={session()?.location.directory} /> - + + data.session.status(sessionID)} + onSelect={(sessionID) => { + if (sessionID !== route.sessionID) navigate({ type: "session", sessionID }) + }} + onEscape={() => { + const parentID = session()?.parentID + if (parentID) { + setChildPicker(true) + navigate({ type: "session", sessionID: parentID }) + return + } + setChildPicker(false) + queueMicrotask(() => prompt?.focus()) + }} + /> + + @@ -916,6 +961,13 @@ export function Session() { ) + } + + return ( + + {(_sessionID: string) => } + + ) } function SessionRowView(props: { row: SessionRow; message: (messageID: string) => SessionMessage | undefined }) {