feat(tui): integrate composer picker

This commit is contained in:
Dax Raad 2026-06-29 15:37:09 -04:00
parent fa73546a86
commit c65a7d50c1
3 changed files with 57 additions and 41 deletions

View file

@ -8,7 +8,7 @@
```bash
# From packages/cli: local V2 TUI
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev --standalone
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev
# Released legacy TUI behavior reference
termctrl start opencode-legacy --host opentui --cols 112 --rows 34 -- bunx opencode-ai@latest
@ -23,12 +23,12 @@ termctrl save opencode-legacy --format png --out /tmp/opencode/legacy.png
## Interactive debugging
- This package is the V2 CLI adapter. Run its `dev` script when testing the TUI; do not use the repository-root `bun dev`, which launches the legacy `packages/opencode` CLI.
- Run commands from `packages/cli`. Use `bun dev --standalone` for most debugging so the TUI starts with a private V2 server instead of depending on the background service.
- Run commands from `packages/cli`. Use `bun dev` for most debugging so the TUI starts with a private V2 server.
- Use `termctrl` for interactive checks instead of starting the TUI as a blocking foreground process. It provides a real PTY, handles OpenTUI's host handshake, and can save reviewable screenshots.
- Use a dedicated session name and do not reuse or kill an unrelated session.
```bash
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev --standalone
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev
termctrl wait opencode-v2-dev "Ask anything" --timeout 20000
termctrl show opencode-v2-dev
```
@ -56,7 +56,7 @@ termctrl show opencode-v2-dev
```
- Source changes may require restarting the process. Use `termctrl restart opencode-v2-dev` rather than assuming the running TUI reloaded the change.
- To exercise background-service behavior, omit `--standalone`. Service lifecycle commands are available through `bun dev service start`, `bun dev service status`, and `bun dev service stop`.
- To exercise background-service behavior, use `bun dev service start`, `bun dev service status`, and `bun dev service stop`.
- Always clean up the Terminal Control session when the check is complete:
```bash
@ -85,7 +85,7 @@ bun dev api <operationId> --param key=value
```bash
termctrl start opencode-v2-debug --host opentui --cols 112 --rows 34 -- \
bun run --inspect=ws://localhost:6499/ src/index.ts --standalone
bun run --inspect=ws://localhost:6499/ src/index.ts
```
- Use `--inspect-wait` or `--inspect-brk` when execution must pause until the debugger attaches.

View file

@ -99,7 +99,7 @@ export const Definitions = {
session_toggle_timestamps: keybind("none", "Toggle message timestamps"),
session_toggle_generic_tool_output: keybind("none", "Toggle generic tool output"),
session_queued_prompts: keybind("<leader>q", "Manage queued prompts"),
session_child_first: keybind("<leader>down", "Go to first child session"),
session_child_first: keybind("<leader>down", "Toggle subagent picker"),
session_child_cycle: keybind("right", "Go to next child session"),
session_child_cycle_reverse: keybind("left", "Go to previous child session"),
session_parent: keybind("up", "Go to parent session"),

View file

@ -16,6 +16,7 @@ import {
import path from "node:path"
import { mkdir, writeFile } from "node:fs/promises"
import { useRoute, useRouteData } from "../../context/route"
import { createStore } from "solid-js/store"
import { useProject } from "../../context/project"
import { useData } from "../../context/data"
import { SplitBorder } from "../../ui/border"
@ -46,7 +47,7 @@ import { DialogSessionRename } from "../../component/dialog-session-rename"
import { TodoItem } from "../../component/todo-item"
import { DialogMessage } from "./dialog-message"
import { Sidebar } from "./sidebar"
import { SubagentFooter } from "./subagent-footer.tsx"
import { Composer } from "./composer"
import { filetype } from "../../util/filetype"
import parsers from "../../parsers-config"
import { errorMessage } from "../../util/error"
@ -177,7 +178,10 @@ 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 [composer, setComposer] = createStore({
open: false,
tab: undefined as string | undefined,
})
const disabled = createMemo(() => permissions().length > 0 || questions().length > 0)
const pending = createMemo(() => {
@ -764,11 +768,14 @@ export function Session() {
run: () => unavailable("Backgrounding subagents"),
},
{
title: "Go to child session",
title: "Toggle subagent picker",
value: "session.child.first",
category: "Session",
hidden: true,
run: () => unavailable("Child session discovery"),
run: () => {
if (composer.open || session()?.parentID) setComposer("open", false)
else setComposer("open", true)
dialog.clear()
},
},
{
title: "Go to parent session",
@ -836,6 +843,7 @@ export function Session() {
// snap to bottom when session changes
createEffect(on(() => route.sessionID, toBottom))
createEffect(on(() => route.sessionID, () => setComposer("open", false)))
return (
<LocationProvider location={location()}>
@ -898,37 +906,45 @@ export function Session() {
</Show>
</scrollbox>
<box flexShrink={0}>
<Show when={permissions().length > 0}>
<PermissionPrompt request={permissions()[0]} directory={session()?.location.directory} />
</Show>
<Show when={permissions().length === 0 && questions().length > 0}>
<QuestionPrompt request={questions()[0]} directory={session()?.location.directory} />
</Show>
<Show when={session()?.parentID}>
<SubagentFooter />
</Show>
<Show when={visible()}>
<pluginRuntime.Slot
name="session_prompt"
mode="replace"
session_id={route.sessionID}
visible={visible()}
disabled={disabled()}
on_submit={toBottom}
ref={bind}
>
<Prompt
visible={visible()}
<Composer
sessionID={route.sessionID}
open={composer.open || !!session()?.parentID}
defaultTab={composer.tab ?? (session()?.parentID ? "subagents" : undefined)}
onClose={() => setComposer("open", false)}
/>
<Switch>
<Match when={composer.open || !!session()?.parentID}>
{null}
</Match>
<Match when={permissions().length > 0}>
<PermissionPrompt request={permissions()[0]} directory={session()?.location.directory} />
</Match>
<Match when={questions().length > 0}>
<QuestionPrompt request={questions()[0]} directory={session()?.location.directory} />
</Match>
<Match when={!disabled()}>
<pluginRuntime.Slot
name="session_prompt"
mode="replace"
session_id={route.sessionID}
visible={true}
disabled={false}
on_submit={toBottom}
ref={bind}
disabled={disabled()}
onSubmit={() => {
toBottom()
}}
sessionID={route.sessionID}
right={<pluginRuntime.Slot name="session_prompt_right" session_id={route.sessionID} />}
/>
</pluginRuntime.Slot>
</Show>
>
<Prompt
visible={true}
ref={bind}
disabled={false}
onSubmit={() => {
toBottom()
}}
sessionID={route.sessionID}
right={<pluginRuntime.Slot name="session_prompt_right" session_id={route.sessionID} />}
/>
</pluginRuntime.Slot>
</Match>
</Switch>
</box>
</Show>
<Toast />