parent
648183cecb
commit
a3e2cc0dcd
16 changed files with 268 additions and 33 deletions
|
|
@ -34,6 +34,7 @@ function runAgent(input: CurrentAgent): RunAgent {
|
|||
return {
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
mode: input.mode,
|
||||
hidden: input.hidden,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type {
|
|||
FooterSubagentTab,
|
||||
MiniSettingChange,
|
||||
MiniSettings,
|
||||
RunAgent,
|
||||
RunCommand,
|
||||
RunInput,
|
||||
RunProvider,
|
||||
|
|
@ -21,6 +22,7 @@ type PanelEntry = RunFooterMenuItem & {
|
|||
}
|
||||
|
||||
type CommandEntry =
|
||||
| (PanelEntry & { action: "agent" })
|
||||
| (PanelEntry & { action: "model" })
|
||||
| (PanelEntry & { action: "editor" })
|
||||
| (PanelEntry & { action: "skill" })
|
||||
|
|
@ -40,6 +42,11 @@ type ModelEntry = PanelEntry & {
|
|||
current: boolean
|
||||
}
|
||||
|
||||
type AgentEntry = PanelEntry & {
|
||||
id: string
|
||||
current: boolean
|
||||
}
|
||||
|
||||
type VariantEntry = PanelEntry & {
|
||||
variant: string | undefined
|
||||
current: boolean
|
||||
|
|
@ -341,6 +348,7 @@ export function RunCommandMenuBody(props: {
|
|||
variants: Accessor<string[]>
|
||||
variantCycle: string
|
||||
onClose: () => void
|
||||
onAgent: () => void
|
||||
onModel: () => void
|
||||
onEditor: () => void
|
||||
onSkill: () => void
|
||||
|
|
@ -419,6 +427,11 @@ export function RunCommandMenuBody(props: {
|
|||
]
|
||||
: []
|
||||
const agent: CommandEntry[] = [
|
||||
{
|
||||
action: "agent",
|
||||
category: "Agent",
|
||||
display: "Switch agent",
|
||||
},
|
||||
{
|
||||
action: "model",
|
||||
category: "Agent",
|
||||
|
|
@ -471,6 +484,11 @@ export function RunCommandMenuBody(props: {
|
|||
]
|
||||
})
|
||||
const pick = (item: CommandEntry) => {
|
||||
if (item.action === "agent") {
|
||||
props.onAgent()
|
||||
return
|
||||
}
|
||||
|
||||
if (item.action === "model") {
|
||||
props.onModel()
|
||||
return
|
||||
|
|
@ -568,6 +586,67 @@ export function RunCommandMenuBody(props: {
|
|||
)
|
||||
}
|
||||
|
||||
export function RunAgentSelectBody(props: {
|
||||
theme: Accessor<RunFooterTheme>
|
||||
agents: Accessor<RunAgent[]>
|
||||
current: Accessor<string | undefined>
|
||||
onClose: () => void
|
||||
onSelect: (agent: string) => void
|
||||
mono?: boolean
|
||||
}) {
|
||||
const entries = createMemo<AgentEntry[]>(() =>
|
||||
props
|
||||
.agents()
|
||||
.filter((agent) => agent.mode !== "subagent" && !agent.hidden)
|
||||
.map((agent) => ({
|
||||
category: "",
|
||||
display: agent.id,
|
||||
description: agent.description,
|
||||
footer: props.current() === agent.id ? "current" : undefined,
|
||||
keywords: `${agent.id} ${agent.name} ${agent.description ?? ""}`,
|
||||
id: agent.id,
|
||||
current: props.current() === agent.id,
|
||||
})),
|
||||
)
|
||||
const controller = createSearchablePanelController({
|
||||
entries,
|
||||
limit: PANEL_LIST_ROWS,
|
||||
onClose: props.onClose,
|
||||
onSelect: (item) => props.onSelect(item.id),
|
||||
isCurrent: (item) => item.current,
|
||||
})
|
||||
|
||||
return (
|
||||
<PanelShell
|
||||
title="Select agent"
|
||||
query={controller.query()}
|
||||
count={controller.items().length}
|
||||
total={entries().length}
|
||||
placeholder="Search"
|
||||
theme={props.theme}
|
||||
inputRef={controller.inputRef}
|
||||
onQuery={controller.setQuery}
|
||||
mono={props.mono}
|
||||
>
|
||||
<RunFooterMenu
|
||||
theme={props.theme}
|
||||
items={controller.items}
|
||||
selected={controller.menu.selected}
|
||||
offset={controller.menu.offset}
|
||||
rows={() => PANEL_LIST_ROWS}
|
||||
limit={PANEL_LIST_ROWS}
|
||||
empty="No agents found"
|
||||
border={false}
|
||||
paddingLeft={panelPad(props.mono)}
|
||||
paddingRight={panelPad(props.mono)}
|
||||
grouped={false}
|
||||
background
|
||||
mono={props.mono}
|
||||
/>
|
||||
</PanelShell>
|
||||
)
|
||||
}
|
||||
|
||||
export function RunSettingsBody(props: {
|
||||
theme: Accessor<RunFooterTheme>
|
||||
settings: Accessor<MiniSettings>
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ type RunFooterOptions = {
|
|||
agents: RunAgent[]
|
||||
references: RunReference[]
|
||||
wrote?: boolean
|
||||
agentLabel: string
|
||||
agent: string | undefined
|
||||
modelLabel: string
|
||||
model: RunInput["model"]
|
||||
variant: string | undefined
|
||||
|
|
@ -91,6 +91,7 @@ type RunFooterOptions = {
|
|||
onFormReply: (input: FormReply) => void | Promise<void>
|
||||
onFormCancel: (input: FormCancel) => void | Promise<void>
|
||||
onCycleVariant?: () => CycleResult | void
|
||||
onAgentSelect?: (agent: string) => void
|
||||
onModelSelect?: (model: NonNullable<RunInput["model"]>) => CycleResult | void | Promise<CycleResult | void>
|
||||
onVariantSelect?: (variant: string | undefined) => CycleResult | void | Promise<CycleResult | void>
|
||||
onInterrupt?: () => void
|
||||
|
|
@ -169,6 +170,9 @@ export class RunFooter implements FooterApi {
|
|||
private setCommands: Setter<RunCommand[] | undefined>
|
||||
private providers: Accessor<RunProvider[] | undefined>
|
||||
private setProviders: Setter<RunProvider[] | undefined>
|
||||
private currentAgent: Accessor<string>
|
||||
private currentAgentID: Accessor<string | undefined>
|
||||
private setCurrentAgentID: Setter<string | undefined>
|
||||
private currentModel: Accessor<RunInput["model"]>
|
||||
private setCurrentModel: Setter<RunInput["model"]>
|
||||
private variants: Accessor<string[]>
|
||||
|
|
@ -194,6 +198,7 @@ export class RunFooter implements FooterApi {
|
|||
private interruptTimeout: NodeJS.Timeout | undefined
|
||||
private exitTimeout: NodeJS.Timeout | undefined
|
||||
private noticeTimeout: NodeJS.Timeout | undefined
|
||||
private turnAgent: string | undefined
|
||||
private requestExitHandler: (() => boolean) | undefined
|
||||
private scrollback: RunScrollbackStream
|
||||
private themes: RunTheme[]
|
||||
|
|
@ -247,6 +252,14 @@ export class RunFooter implements FooterApi {
|
|||
const [providers, setProviders] = createSignal<RunProvider[] | undefined>()
|
||||
this.providers = providers
|
||||
this.setProviders = setProviders
|
||||
const [currentAgentID, setCurrentAgentID] = createSignal(options.agent)
|
||||
this.currentAgentID = currentAgentID
|
||||
this.setCurrentAgentID = setCurrentAgentID
|
||||
this.currentAgent = () => {
|
||||
const agent = currentAgentID()
|
||||
if (!agent) return "Default"
|
||||
return this.agents().find((item) => item.id === agent)?.name ?? Locale.titlecase(agent)
|
||||
}
|
||||
const [currentModel, setCurrentModel] = createSignal<RunInput["model"]>(options.model)
|
||||
this.currentModel = currentModel
|
||||
this.setCurrentModel = setCurrentModel
|
||||
|
|
@ -305,6 +318,8 @@ export class RunFooter implements FooterApi {
|
|||
references: footer.references,
|
||||
commands: footer.commands,
|
||||
providers: footer.providers,
|
||||
currentAgent: footer.currentAgent,
|
||||
currentAgentID: footer.currentAgentID,
|
||||
currentModel: footer.currentModel,
|
||||
variants: footer.variants,
|
||||
currentVariant: footer.currentVariant,
|
||||
|
|
@ -324,6 +339,7 @@ export class RunFooter implements FooterApi {
|
|||
onExitRequest: footer.handleExit,
|
||||
onRequestExit: footer.setRequestExitHandler,
|
||||
onExit: () => footer.close(),
|
||||
onAgentSelect: footer.handleAgentSelect,
|
||||
onModelSelect: footer.handleModelSelect,
|
||||
onVariantSelect: footer.handleVariantSelect,
|
||||
onRows: footer.syncRows,
|
||||
|
|
@ -377,7 +393,7 @@ export class RunFooter implements FooterApi {
|
|||
}
|
||||
|
||||
if (next.type === "agent") {
|
||||
this.options.agentLabel = Locale.titlecase(next.agent ?? "build")
|
||||
this.setCurrentAgentID(next.agent)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -386,13 +402,15 @@ export class RunFooter implements FooterApi {
|
|||
}
|
||||
|
||||
if (next.type === "turn.duration") {
|
||||
const agent = this.turnAgent ?? this.currentAgent()
|
||||
this.turnAgent = undefined
|
||||
if (this.miniSettings().turn_summary === "hide") return
|
||||
const current = this.currentModel()
|
||||
this.flush()
|
||||
this.flushing = this.flushing
|
||||
.then(() =>
|
||||
this.scrollback.writeTurnSummary({
|
||||
agent: this.options.agentLabel,
|
||||
agent,
|
||||
model: current ? modelInfo(this.providers(), current).model : this.state().model,
|
||||
duration: next.duration,
|
||||
}),
|
||||
|
|
@ -451,6 +469,7 @@ export class RunFooter implements FooterApi {
|
|||
patch.notice = ""
|
||||
}
|
||||
if (next.type === "turn.send") {
|
||||
this.turnAgent = this.currentAgent()
|
||||
this.clearInterruptTimer()
|
||||
this.clearExitTimer()
|
||||
}
|
||||
|
|
@ -667,7 +686,7 @@ export class RunFooter implements FooterApi {
|
|||
? this.base + PERMISSION_ROWS
|
||||
: type === "form"
|
||||
? this.base + FORM_ROWS
|
||||
: ["command", "skill", "model", "variant", "settings"].includes(route)
|
||||
: ["command", "skill", "agent", "model", "variant", "settings"].includes(route)
|
||||
? 1 + RUN_COMMAND_PANEL_ROWS
|
||||
: route === "queued-menu" || route === "subagent-menu"
|
||||
? 1 + this.subagentMenuRows
|
||||
|
|
@ -815,6 +834,13 @@ export class RunFooter implements FooterApi {
|
|||
.catch(() => {})
|
||||
}
|
||||
|
||||
private handleAgentSelect = (agent: string): void => {
|
||||
if (this.isClosed || this.currentAgentID() === agent) return
|
||||
this.setCurrentAgentID(agent)
|
||||
this.options.onAgentSelect?.(agent)
|
||||
this.setNotice(`agent ${this.currentAgent()}`)
|
||||
}
|
||||
|
||||
private handleVariantSelect = (variant: string | undefined): void => {
|
||||
if (this.isClosed) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { registerOpencodeSpinner } from "../component/register-spinner"
|
|||
import { createColors, createFrames } from "../ui/spinner"
|
||||
import {
|
||||
RUN_SUBAGENT_PANEL_ROWS,
|
||||
RunAgentSelectBody,
|
||||
RunCommandMenuBody,
|
||||
RunModelSelectBody,
|
||||
RunQueuedPromptSelectBody,
|
||||
|
|
@ -76,6 +77,8 @@ type RunFooterViewProps = {
|
|||
references: () => RunReference[]
|
||||
commands: () => RunCommand[] | undefined
|
||||
providers: () => RunProvider[] | undefined
|
||||
currentAgent: () => string
|
||||
currentAgentID: () => string | undefined
|
||||
currentModel: () => RunInput["model"]
|
||||
variants: () => string[]
|
||||
currentVariant: () => string | undefined
|
||||
|
|
@ -99,6 +102,7 @@ type RunFooterViewProps = {
|
|||
onExitRequest?: () => boolean
|
||||
onRequestExit?: (fn: (() => boolean) | undefined) => void
|
||||
onExit: () => void
|
||||
onAgentSelect: (agent: string) => void
|
||||
onModelSelect: (model: NonNullable<RunInput["model"]>) => void
|
||||
onVariantSelect: (variant: string | undefined) => void
|
||||
onRows: (rows: number) => void
|
||||
|
|
@ -134,6 +138,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
const inspecting = createMemo(() => active().type === "prompt" && route().type === "subagent")
|
||||
const commanding = createMemo(() => active().type === "prompt" && route().type === "command")
|
||||
const skilling = createMemo(() => active().type === "prompt" && route().type === "skill")
|
||||
const agenting = createMemo(() => active().type === "prompt" && route().type === "agent")
|
||||
const modeling = createMemo(() => active().type === "prompt" && route().type === "model")
|
||||
const varianting = createMemo(() => active().type === "prompt" && route().type === "variant")
|
||||
const setting = createMemo(() => active().type === "prompt" && route().type === "settings")
|
||||
|
|
@ -145,6 +150,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
selectingSubagent() ||
|
||||
commanding() ||
|
||||
skilling() ||
|
||||
agenting() ||
|
||||
modeling() ||
|
||||
varianting() ||
|
||||
setting(),
|
||||
|
|
@ -219,7 +225,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
const footerStatus = createMemo(() => {
|
||||
const current = model() ?? props.state().model.trim()
|
||||
const variant = props.currentVariant()
|
||||
const details = [busy() ? "running" : "idle"]
|
||||
const details = [busy() ? "running" : "idle", `agent ${props.currentAgent()}`]
|
||||
if (current) details.push(variant ? `${current} ${variant}` : current)
|
||||
if (usage()) details.push(props.mono ? usage().replaceAll(" · ", " - ") : usage())
|
||||
if (queuedPrompts().length > 0) details.push(`${queuedPrompts().length} pending`)
|
||||
|
|
@ -268,6 +274,11 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
props.onSubagentSelect?.(undefined)
|
||||
}
|
||||
|
||||
const openAgent = () => {
|
||||
setRoute({ type: "agent" })
|
||||
props.onSubagentSelect?.(undefined)
|
||||
}
|
||||
|
||||
const openSkillMenu = () => {
|
||||
if (props.commands() && skills().length === 0) {
|
||||
return
|
||||
|
|
@ -407,8 +418,9 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
})
|
||||
const modelStatus = createMemo(() => {
|
||||
const current = model() ?? props.state().model.trim()
|
||||
if (!footerDetails() || !prompt() || !responsive().statusline.showModel || !current) return
|
||||
if (!footerDetails() || !prompt() || shell() || !responsive().statusline.showModel || !current) return
|
||||
return {
|
||||
agent: props.currentAgent(),
|
||||
model: current,
|
||||
variant: responsive().statusline.showModelVariant ? props.currentVariant() : undefined,
|
||||
}
|
||||
|
|
@ -593,6 +605,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
if (
|
||||
current.type !== "command" &&
|
||||
current.type !== "skill" &&
|
||||
current.type !== "agent" &&
|
||||
current.type !== "model" &&
|
||||
current.type !== "variant" &&
|
||||
current.type !== "settings" &&
|
||||
|
|
@ -698,6 +711,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
variants={props.variants}
|
||||
variantCycle={variantCycle()}
|
||||
onClose={closePanel}
|
||||
onAgent={openAgent}
|
||||
onModel={openModel}
|
||||
onEditor={() => {
|
||||
closePanel()
|
||||
|
|
@ -748,6 +762,19 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
mono={props.mono}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={agenting()}>
|
||||
<RunAgentSelectBody
|
||||
theme={theme}
|
||||
agents={props.agents}
|
||||
current={props.currentAgentID}
|
||||
onClose={closePanel}
|
||||
onSelect={(agent) => {
|
||||
props.onAgentSelect(agent)
|
||||
closePanel()
|
||||
}}
|
||||
mono={props.mono}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={modeling()}>
|
||||
<RunModelSelectBody
|
||||
theme={theme}
|
||||
|
|
@ -907,6 +934,10 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
flexShrink={1}
|
||||
>
|
||||
<text fg={theme().text} wrapMode="none" truncate>
|
||||
<Show when={responsive().statusline.showAgent}>
|
||||
{info().agent}
|
||||
<span style={{ fg: theme().muted }}>{props.mono ? " - " : " · "}</span>
|
||||
</Show>
|
||||
{info().model}
|
||||
<Show when={info().variant}>
|
||||
{(variant) => <span style={{ fg: theme().warning, bold: true }}> {variant()}</span>}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export function footerWidthPolicy(width: number) {
|
|||
},
|
||||
statusline: {
|
||||
showActivityMeta: compact,
|
||||
showAgent: compact,
|
||||
showCommandHint: width >= FOOTER_WIDTH_BREAKPOINTS.commandHint,
|
||||
showModel: width >= FOOTER_WIDTH_BREAKPOINTS.model,
|
||||
showModelVariant: width >= FOOTER_WIDTH_BREAKPOINTS.modelVariant,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
import path from "path"
|
||||
import { CliRenderEvents, createCliRenderer, type CliRenderer, type ScrollbackWriter } from "@opentui/core"
|
||||
import { isDefaultTitle } from "../util/session"
|
||||
import { Locale } from "../util/locale"
|
||||
import { entrySplash, exitSplash, splashMeta } from "./splash"
|
||||
import { resolveRunTheme } from "./theme"
|
||||
import type {
|
||||
|
|
@ -45,11 +44,6 @@ type CycleResult = {
|
|||
variants?: string[]
|
||||
}
|
||||
|
||||
type FooterLabels = {
|
||||
agentLabel: string
|
||||
modelLabel: string
|
||||
}
|
||||
|
||||
export type LifecycleInput = {
|
||||
host: MiniHost
|
||||
getDirectory: () => string
|
||||
|
|
@ -70,6 +64,7 @@ export type LifecycleInput = {
|
|||
onFormReply: (input: FormReply) => void | Promise<void>
|
||||
onFormCancel: (input: FormCancel) => void | Promise<void>
|
||||
onCycleVariant?: () => CycleResult | void
|
||||
onAgentSelect?: (agent: string) => void
|
||||
onModelSelect?: (model: NonNullable<RunInput["model"]>) => CycleResult | void | Promise<CycleResult | void>
|
||||
onVariantSelect?: (variant: string | undefined) => CycleResult | void | Promise<CycleResult | void>
|
||||
onInterrupt?: () => void
|
||||
|
|
@ -123,14 +118,6 @@ function splashInfo(title: string | undefined, history: RunPrompt[]) {
|
|||
}
|
||||
}
|
||||
|
||||
function footerLabels(input: Pick<RunInput, "agent" | "model" | "variant">): FooterLabels {
|
||||
const agentLabel = Locale.titlecase(input.agent ?? "build")
|
||||
return {
|
||||
agentLabel,
|
||||
modelLabel: input.model ? formatModelLabel(input.model, input.variant) : "Default model",
|
||||
}
|
||||
}
|
||||
|
||||
function directoryLabel(directory: string, home: string) {
|
||||
const resolved = path.resolve(directory)
|
||||
const display =
|
||||
|
|
@ -203,11 +190,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
|||
session_id: input.sessionID,
|
||||
mono,
|
||||
})
|
||||
const labels = footerLabels({
|
||||
agent: input.agent,
|
||||
model: input.model,
|
||||
variant: input.variant,
|
||||
})
|
||||
const wrote = queueSplash(
|
||||
renderer,
|
||||
state,
|
||||
|
|
@ -232,7 +214,8 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
|||
findFiles: input.findFiles,
|
||||
agents: input.agents,
|
||||
references: input.references,
|
||||
...labels,
|
||||
agent: input.agent,
|
||||
modelLabel: input.model ? formatModelLabel(input.model, input.variant) : "Default model",
|
||||
model: input.model,
|
||||
variant: input.variant,
|
||||
first: input.first,
|
||||
|
|
@ -249,6 +232,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
|||
onFormReply: input.onFormReply,
|
||||
onFormCancel: input.onFormCancel,
|
||||
onCycleVariant: input.onCycleVariant,
|
||||
onAgentSelect: input.onAgentSelect,
|
||||
onModelSelect: input.onModelSelect,
|
||||
onVariantSelect: input.onVariantSelect,
|
||||
onInterrupt: input.onInterrupt,
|
||||
|
|
|
|||
|
|
@ -304,6 +304,9 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
|||
variant: state.activeVariant,
|
||||
}
|
||||
},
|
||||
onAgentSelect: (agent) => {
|
||||
state.agent = agent
|
||||
},
|
||||
onModelSelect: async (model) => {
|
||||
if (state.model?.providerID === model.providerID && state.model.modelID === model.modelID) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1553,6 +1553,8 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
throw new Error("This prompt cannot be queued")
|
||||
if (!state.connected) throw new Error("Event stream is reconnecting")
|
||||
const client = sdk
|
||||
if (next.agent)
|
||||
await client.session.switchAgent({ sessionID: input.sessionID, agent: next.agent }, { signal: next.signal })
|
||||
mergePending(await admitPrompt(next, client, "queue"))
|
||||
settlementClient = client
|
||||
},
|
||||
|
|
@ -1574,6 +1576,8 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
|
||||
const command = next.prompt.command
|
||||
if (command?.source === "skill") {
|
||||
if (next.agent)
|
||||
await client.session.switchAgent({ sessionID: input.sessionID, agent: next.agent }, { signal: next.signal })
|
||||
input.trace?.write("send.skill", { sessionID: input.sessionID, messageID, skill: command.name })
|
||||
await runTurnWait(
|
||||
next,
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ export type FooterQueuedPrompt = {
|
|||
export type RunAgent = {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
mode: "subagent" | "primary" | "all"
|
||||
hidden: boolean
|
||||
}
|
||||
|
|
@ -286,6 +287,7 @@ export type FooterPromptRoute =
|
|||
| { type: "subagent"; sessionID: string }
|
||||
| { type: "command" }
|
||||
| { type: "skill" }
|
||||
| { type: "agent" }
|
||||
| { type: "model" }
|
||||
| { type: "variant" }
|
||||
| { type: "settings" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue