Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton 0a8402832e fix(tui): open subagent panel from footer 2026-08-18 23:00:30 -04:00
2 changed files with 58 additions and 12 deletions
@@ -1,5 +1,5 @@
import { Plugin } from "@opencode-ai/plugin/tui"
import { createMemo, Match, Show, Switch } from "solid-js"
import { createMemo, createSignal, Match, Show, Switch } from "solid-js"
import { contextUsage, formatContextUsage } from "../../util/session"
import { useTerminalDimensions } from "@opentui/solid"
@@ -10,6 +10,7 @@ const money = new Intl.NumberFormat("en-US", {
export function PromptFooter(props: { context: Plugin.Context; sessionID?: string; mode: "normal" | "shell" }) {
const dimensions = useTerminalDimensions()
const [liveHovered, setLiveHovered] = createSignal(false)
const subagents = createMemo(() => {
if (!props.sessionID) return 0
const count = props.context.data.session
@@ -47,16 +48,43 @@ export function PromptFooter(props: { context: Plugin.Context; sessionID?: strin
<Match when={props.mode === "normal"}>
<Switch>
<Match when={live() || status().length > 0}>
<text fg={props.context.theme.text.subdued} wrapMode="none" truncate flexShrink={1}>
<Show when={live() && shortcut("session.child.first")}>
{(value) => <span style={{ fg: props.context.theme.text.default }}>{value()} </span>}
<box flexDirection="row" flexShrink={1} minWidth={0}>
<Show when={live()}>
<box
flexDirection="row"
flexShrink={0}
backgroundColor={
liveHovered()
? props.context.theme.background.action.primary.focused
: props.context.theme.background.action.primary.default
}
onMouseOver={() => setLiveHovered(true)}
onMouseOut={() => setLiveHovered(false)}
onMouseDown={() => setLiveHovered(true)}
onMouseUp={() => props.context.keymap.dispatch("session.child.first")}
>
<text
fg={
liveHovered()
? props.context.theme.text.action.primary.focused
: props.context.theme.text.action.primary.default
}
wrapMode="none"
>
<Show when={shortcut("session.child.first")}>{(value) => <>{value()} </>}</Show>
<Show when={subagents()}>{(value) => <>{value()}</>}</Show>
<Show when={subagents() && shells()}> · </Show>
<Show when={shells()}>{(value) => <>{value()}</>}</Show>
</text>
</box>
</Show>
<Show when={subagents()}>{(value) => <span>{value()}</span>}</Show>
<Show when={subagents() && shells()}> · </Show>
<Show when={shells()}>{(value) => <span>{value()}</span>}</Show>
<Show when={live() && status().length > 0}> · </Show>
<Show when={status().length > 0}>{status().join(" · ")}</Show>
</text>
<Show when={status().length > 0}>
<text fg={props.context.theme.text.subdued} wrapMode="none" truncate flexShrink={1}>
<Show when={live()}> · </Show>
{status().join(" · ")}
</text>
</Show>
</box>
</Match>
<Match when={dimensions().width >= 44}>
<text fg={props.context.theme.text.default} flexShrink={0}>
@@ -1,18 +1,28 @@
/** @jsxImportSource @opentui/solid */
import { expect, test } from "bun:test"
import { RGBA } from "@opentui/core"
import { BoxRenderable, RGBA } from "@opentui/core"
import { testRender } from "@opentui/solid"
import type { Context } from "@opencode-ai/plugin/tui/context"
import { PromptFooter } from "../../src/feature-plugins/prompt/footer"
test("prompt footer separates simultaneous subagent, shell, and usage status", async () => {
const color = RGBA.fromInts(200, 200, 200)
const focused = RGBA.fromInts(40, 40, 40)
const dispatched: string[] = []
const context = {
location: { directory: "/workspace" },
theme: { text: { default: color, subdued: color } },
theme: {
text: {
default: color,
subdued: color,
action: { primary: { default: color, focused: color } },
},
background: { action: { primary: { default: RGBA.fromInts(0, 0, 0, 0), focused } } },
},
keymap: {
shortcuts: (id: string) =>
id === "session.child.first" ? ["ctrl+j"] : id === "command.palette.show" ? ["ctrl+p"] : [],
dispatch: (id: string) => dispatched.push(id),
},
data: {
session: {
@@ -39,6 +49,14 @@ test("prompt footer separates simultaneous subagent, shell, and usage status", a
await app.renderOnce()
expect(app.captureCharFrame()).toContain("ctrl+j 1 subagent · 1 shell · $1.00")
expect(app.captureCharFrame()).toContain("ctrl+p commands")
await app.mockMouse.moveTo(2, 0)
const live = app.renderer.root.getChildren()[0]?.getChildren()[0]
expect(live).toBeInstanceOf(BoxRenderable)
expect((live as BoxRenderable).backgroundColor.toInts()).toEqual(focused.toInts())
await app.mockMouse.click(2, 0)
expect(dispatched).toEqual(["session.child.first"])
} finally {
app.renderer.destroy()
}