+16
![opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)



![opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)




Aiden Cline
Frank
Aarav Sareen
Brendan Allan
opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Jack
Brendan Allan
Shoubhit Dash
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
James Long
Dustin Deus
starptech
Luke Parker
𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴
Dax
usrnk1
Jay
runvip
opencode
Julian Coy
Vladimir Glafirov
Adam
Kit Langton
Simon Klee
Jay
David Hill
9e0d3976e1
Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: James Long <longster@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: Jay <air@live.ca> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com>
191 lines
5.6 KiB
TypeScript
191 lines
5.6 KiB
TypeScript
/** @jsxImportSource @opentui/solid */
|
|
import type { ScrollBoxRenderable } from "@opentui/core"
|
|
import { useKeyboard } from "@opentui/solid"
|
|
import { registerOpencodeSpinner } from "@opencode-ai/tui/component/register-spinner"
|
|
import { Show, createMemo, indexArray } from "solid-js"
|
|
import { SPINNER_FRAMES } from "@opencode-ai/tui/component/spinner"
|
|
import { RunEntryContent, separatorRows } from "./scrollback.writer"
|
|
import type { FooterSubagentDetail, FooterSubagentTab, RunDiffStyle } from "./types"
|
|
import type { RunFooterTheme, RunTheme } from "./theme"
|
|
|
|
registerOpencodeSpinner()
|
|
|
|
export const SUBAGENT_INSPECTOR_ROWS = 14
|
|
|
|
function statusColor(theme: RunFooterTheme, status: FooterSubagentTab["status"]) {
|
|
if (status === "completed") {
|
|
return theme.highlight
|
|
}
|
|
|
|
if (status === "cancelled") {
|
|
return theme.muted
|
|
}
|
|
|
|
if (status === "error") {
|
|
return theme.error
|
|
}
|
|
|
|
return theme.highlight
|
|
}
|
|
|
|
function statusIcon(status: FooterSubagentTab["status"]) {
|
|
if (status === "completed") {
|
|
return "●"
|
|
}
|
|
|
|
if (status === "cancelled") {
|
|
return "○"
|
|
}
|
|
|
|
if (status === "error") {
|
|
return "◍"
|
|
}
|
|
|
|
return "◔"
|
|
}
|
|
|
|
export function RunFooterSubagentBody(props: {
|
|
active: () => boolean
|
|
theme: () => RunTheme
|
|
tab: () => FooterSubagentTab | undefined
|
|
index: () => number
|
|
total: () => number
|
|
detail: () => FooterSubagentDetail | undefined
|
|
width: () => number
|
|
diffStyle?: RunDiffStyle
|
|
onCycle: (dir: -1 | 1) => void
|
|
onClose: () => void
|
|
// Formatted interrupt shortcut from the registered keymap binding; the
|
|
// command itself is dispatched through the keymap in footer.view.
|
|
interrupt?: () => string | undefined
|
|
}) {
|
|
const theme = createMemo(() => props.theme())
|
|
const footer = createMemo(() => theme().footer)
|
|
const tab = createMemo(() => props.tab())
|
|
const commits = createMemo(() => props.detail()?.commits ?? [])
|
|
const opts = createMemo(() => ({ diffStyle: props.diffStyle }))
|
|
const scrollbar = createMemo(() => ({
|
|
trackOptions: {
|
|
backgroundColor: footer().surface,
|
|
foregroundColor: footer().line,
|
|
},
|
|
}))
|
|
const title = createMemo(() => {
|
|
const current = tab()
|
|
if (!current) {
|
|
return ""
|
|
}
|
|
|
|
return current.description || current.title || current.label
|
|
})
|
|
const subtitle = createMemo(() => {
|
|
const current = tab()
|
|
if (!current || title() === current.label) {
|
|
return ""
|
|
}
|
|
|
|
return current.label
|
|
})
|
|
const rows = indexArray(commits, (commit, index) => (
|
|
<box flexDirection="column" gap={0} flexShrink={0}>
|
|
{index > 0 && separatorRows(commits()[index - 1], commit()) > 0 ? <box height={1} flexShrink={0} /> : null}
|
|
<RunEntryContent commit={commit()} theme={theme()} opts={opts()} width={props.width()} />
|
|
</box>
|
|
))
|
|
let scroll: ScrollBoxRenderable | undefined
|
|
|
|
const interruptHint = createMemo(() => {
|
|
if (tab()?.status !== "running") return undefined
|
|
return props.interrupt?.()
|
|
})
|
|
|
|
useKeyboard((event) => {
|
|
if (!props.active()) {
|
|
return
|
|
}
|
|
|
|
if (event.name === "escape") {
|
|
event.preventDefault()
|
|
props.onClose()
|
|
return
|
|
}
|
|
|
|
if (event.name === "tab" && !event.shift) {
|
|
event.preventDefault()
|
|
props.onCycle(1)
|
|
return
|
|
}
|
|
|
|
if (event.name === "up" || event.name === "k") {
|
|
event.preventDefault()
|
|
scroll?.scrollBy(-1)
|
|
return
|
|
}
|
|
|
|
if (event.name === "down" || event.name === "j") {
|
|
event.preventDefault()
|
|
scroll?.scrollBy(1)
|
|
}
|
|
})
|
|
|
|
return (
|
|
<box width="100%" height="100%" flexDirection="column" backgroundColor={footer().surface}>
|
|
<box paddingTop={1} paddingLeft={1} paddingRight={3} paddingBottom={1} flexDirection="column" flexGrow={1}>
|
|
<Show when={tab()}>
|
|
{(current) => (
|
|
<box width="100%" flexDirection="row" gap={1} paddingBottom={1} flexShrink={0}>
|
|
{current().status === "running" ? (
|
|
<box flexShrink={0}>
|
|
<spinner frames={SPINNER_FRAMES} interval={80} color={statusColor(footer(), current().status)} />
|
|
</box>
|
|
) : (
|
|
<text fg={statusColor(footer(), current().status)} wrapMode="none" truncate flexShrink={0}>
|
|
{statusIcon(current().status)}
|
|
</text>
|
|
)}
|
|
<text fg={footer().text} wrapMode="none" truncate flexGrow={1} flexShrink={1}>
|
|
{title()}
|
|
<Show when={subtitle().length > 0}>
|
|
<span style={{ fg: footer().muted }}>{" " + subtitle()}</span>
|
|
</Show>
|
|
</text>
|
|
<Show when={interruptHint()}>
|
|
{(hint) => (
|
|
<text fg={footer().muted} wrapMode="none" truncate flexShrink={0}>
|
|
{hint()} interrupt
|
|
</text>
|
|
)}
|
|
</Show>
|
|
<Show when={props.total() > 1 && props.index() > 0}>
|
|
<text fg={footer().muted} wrapMode="none" truncate flexShrink={0}>
|
|
{props.index()} of {props.total()}
|
|
</text>
|
|
</Show>
|
|
</box>
|
|
)}
|
|
</Show>
|
|
<scrollbox
|
|
width="100%"
|
|
height="100%"
|
|
stickyScroll={true}
|
|
stickyStart="bottom"
|
|
verticalScrollbarOptions={scrollbar()}
|
|
ref={(item) => {
|
|
scroll = item
|
|
}}
|
|
>
|
|
<box width="100%" flexDirection="column" gap={0}>
|
|
{commits().length > 0 ? (
|
|
rows()
|
|
) : (
|
|
<text fg={footer().muted} wrapMode="word">
|
|
No subagent activity yet
|
|
</text>
|
|
)}
|
|
</box>
|
|
</scrollbox>
|
|
</box>
|
|
</box>
|
|
)
|
|
}
|