parent
648183cecb
commit
a3e2cc0dcd
16 changed files with 268 additions and 33 deletions
|
|
@ -74,7 +74,7 @@ export async function resolveSessionTarget(input: {
|
||||||
session,
|
session,
|
||||||
location,
|
location,
|
||||||
model: prepared.model,
|
model: prepared.model,
|
||||||
agent: prepared.agent,
|
agent: prepared.agent ?? session.agent,
|
||||||
resume: selected !== undefined,
|
resume: selected !== undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,15 @@ describe("session target resolver", () => {
|
||||||
expect(order).toEqual(["prepare", "create"])
|
expect(order).toEqual(["prepare", "create"])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("uses the agent resolved by the server for a fresh Session", async () => {
|
||||||
|
const client = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||||
|
spyOn(client.location, "get").mockResolvedValue(location("/project"))
|
||||||
|
spyOn(client.session, "create").mockResolvedValue({ ...session("ses_fresh", "/project"), agent: "review" })
|
||||||
|
|
||||||
|
const target = await resolveSessionTarget({ client, prepare })
|
||||||
|
expect(target.agent).toBe("review")
|
||||||
|
})
|
||||||
|
|
||||||
test("does not retry an ambiguous Session creation", async () => {
|
test("does not retry an ambiguous Session creation", async () => {
|
||||||
const client = OpenCode.make({ baseUrl: "https://opencode.test" })
|
const client = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||||
spyOn(client.location, "get").mockResolvedValue(location("/project"))
|
spyOn(client.location, "get").mockResolvedValue(location("/project"))
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ function runAgent(input: CurrentAgent): RunAgent {
|
||||||
return {
|
return {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
|
description: input.description,
|
||||||
mode: input.mode,
|
mode: input.mode,
|
||||||
hidden: input.hidden,
|
hidden: input.hidden,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import type {
|
||||||
FooterSubagentTab,
|
FooterSubagentTab,
|
||||||
MiniSettingChange,
|
MiniSettingChange,
|
||||||
MiniSettings,
|
MiniSettings,
|
||||||
|
RunAgent,
|
||||||
RunCommand,
|
RunCommand,
|
||||||
RunInput,
|
RunInput,
|
||||||
RunProvider,
|
RunProvider,
|
||||||
|
|
@ -21,6 +22,7 @@ type PanelEntry = RunFooterMenuItem & {
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandEntry =
|
type CommandEntry =
|
||||||
|
| (PanelEntry & { action: "agent" })
|
||||||
| (PanelEntry & { action: "model" })
|
| (PanelEntry & { action: "model" })
|
||||||
| (PanelEntry & { action: "editor" })
|
| (PanelEntry & { action: "editor" })
|
||||||
| (PanelEntry & { action: "skill" })
|
| (PanelEntry & { action: "skill" })
|
||||||
|
|
@ -40,6 +42,11 @@ type ModelEntry = PanelEntry & {
|
||||||
current: boolean
|
current: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AgentEntry = PanelEntry & {
|
||||||
|
id: string
|
||||||
|
current: boolean
|
||||||
|
}
|
||||||
|
|
||||||
type VariantEntry = PanelEntry & {
|
type VariantEntry = PanelEntry & {
|
||||||
variant: string | undefined
|
variant: string | undefined
|
||||||
current: boolean
|
current: boolean
|
||||||
|
|
@ -341,6 +348,7 @@ export function RunCommandMenuBody(props: {
|
||||||
variants: Accessor<string[]>
|
variants: Accessor<string[]>
|
||||||
variantCycle: string
|
variantCycle: string
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
onAgent: () => void
|
||||||
onModel: () => void
|
onModel: () => void
|
||||||
onEditor: () => void
|
onEditor: () => void
|
||||||
onSkill: () => void
|
onSkill: () => void
|
||||||
|
|
@ -419,6 +427,11 @@ export function RunCommandMenuBody(props: {
|
||||||
]
|
]
|
||||||
: []
|
: []
|
||||||
const agent: CommandEntry[] = [
|
const agent: CommandEntry[] = [
|
||||||
|
{
|
||||||
|
action: "agent",
|
||||||
|
category: "Agent",
|
||||||
|
display: "Switch agent",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
action: "model",
|
action: "model",
|
||||||
category: "Agent",
|
category: "Agent",
|
||||||
|
|
@ -471,6 +484,11 @@ export function RunCommandMenuBody(props: {
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
const pick = (item: CommandEntry) => {
|
const pick = (item: CommandEntry) => {
|
||||||
|
if (item.action === "agent") {
|
||||||
|
props.onAgent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (item.action === "model") {
|
if (item.action === "model") {
|
||||||
props.onModel()
|
props.onModel()
|
||||||
return
|
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: {
|
export function RunSettingsBody(props: {
|
||||||
theme: Accessor<RunFooterTheme>
|
theme: Accessor<RunFooterTheme>
|
||||||
settings: Accessor<MiniSettings>
|
settings: Accessor<MiniSettings>
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ type RunFooterOptions = {
|
||||||
agents: RunAgent[]
|
agents: RunAgent[]
|
||||||
references: RunReference[]
|
references: RunReference[]
|
||||||
wrote?: boolean
|
wrote?: boolean
|
||||||
agentLabel: string
|
agent: string | undefined
|
||||||
modelLabel: string
|
modelLabel: string
|
||||||
model: RunInput["model"]
|
model: RunInput["model"]
|
||||||
variant: string | undefined
|
variant: string | undefined
|
||||||
|
|
@ -91,6 +91,7 @@ type RunFooterOptions = {
|
||||||
onFormReply: (input: FormReply) => void | Promise<void>
|
onFormReply: (input: FormReply) => void | Promise<void>
|
||||||
onFormCancel: (input: FormCancel) => void | Promise<void>
|
onFormCancel: (input: FormCancel) => void | Promise<void>
|
||||||
onCycleVariant?: () => CycleResult | void
|
onCycleVariant?: () => CycleResult | void
|
||||||
|
onAgentSelect?: (agent: string) => void
|
||||||
onModelSelect?: (model: NonNullable<RunInput["model"]>) => CycleResult | void | Promise<CycleResult | void>
|
onModelSelect?: (model: NonNullable<RunInput["model"]>) => CycleResult | void | Promise<CycleResult | void>
|
||||||
onVariantSelect?: (variant: string | undefined) => CycleResult | void | Promise<CycleResult | void>
|
onVariantSelect?: (variant: string | undefined) => CycleResult | void | Promise<CycleResult | void>
|
||||||
onInterrupt?: () => void
|
onInterrupt?: () => void
|
||||||
|
|
@ -169,6 +170,9 @@ export class RunFooter implements FooterApi {
|
||||||
private setCommands: Setter<RunCommand[] | undefined>
|
private setCommands: Setter<RunCommand[] | undefined>
|
||||||
private providers: Accessor<RunProvider[] | undefined>
|
private providers: Accessor<RunProvider[] | undefined>
|
||||||
private setProviders: Setter<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 currentModel: Accessor<RunInput["model"]>
|
||||||
private setCurrentModel: Setter<RunInput["model"]>
|
private setCurrentModel: Setter<RunInput["model"]>
|
||||||
private variants: Accessor<string[]>
|
private variants: Accessor<string[]>
|
||||||
|
|
@ -194,6 +198,7 @@ export class RunFooter implements FooterApi {
|
||||||
private interruptTimeout: NodeJS.Timeout | undefined
|
private interruptTimeout: NodeJS.Timeout | undefined
|
||||||
private exitTimeout: NodeJS.Timeout | undefined
|
private exitTimeout: NodeJS.Timeout | undefined
|
||||||
private noticeTimeout: NodeJS.Timeout | undefined
|
private noticeTimeout: NodeJS.Timeout | undefined
|
||||||
|
private turnAgent: string | undefined
|
||||||
private requestExitHandler: (() => boolean) | undefined
|
private requestExitHandler: (() => boolean) | undefined
|
||||||
private scrollback: RunScrollbackStream
|
private scrollback: RunScrollbackStream
|
||||||
private themes: RunTheme[]
|
private themes: RunTheme[]
|
||||||
|
|
@ -247,6 +252,14 @@ export class RunFooter implements FooterApi {
|
||||||
const [providers, setProviders] = createSignal<RunProvider[] | undefined>()
|
const [providers, setProviders] = createSignal<RunProvider[] | undefined>()
|
||||||
this.providers = providers
|
this.providers = providers
|
||||||
this.setProviders = setProviders
|
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)
|
const [currentModel, setCurrentModel] = createSignal<RunInput["model"]>(options.model)
|
||||||
this.currentModel = currentModel
|
this.currentModel = currentModel
|
||||||
this.setCurrentModel = setCurrentModel
|
this.setCurrentModel = setCurrentModel
|
||||||
|
|
@ -305,6 +318,8 @@ export class RunFooter implements FooterApi {
|
||||||
references: footer.references,
|
references: footer.references,
|
||||||
commands: footer.commands,
|
commands: footer.commands,
|
||||||
providers: footer.providers,
|
providers: footer.providers,
|
||||||
|
currentAgent: footer.currentAgent,
|
||||||
|
currentAgentID: footer.currentAgentID,
|
||||||
currentModel: footer.currentModel,
|
currentModel: footer.currentModel,
|
||||||
variants: footer.variants,
|
variants: footer.variants,
|
||||||
currentVariant: footer.currentVariant,
|
currentVariant: footer.currentVariant,
|
||||||
|
|
@ -324,6 +339,7 @@ export class RunFooter implements FooterApi {
|
||||||
onExitRequest: footer.handleExit,
|
onExitRequest: footer.handleExit,
|
||||||
onRequestExit: footer.setRequestExitHandler,
|
onRequestExit: footer.setRequestExitHandler,
|
||||||
onExit: () => footer.close(),
|
onExit: () => footer.close(),
|
||||||
|
onAgentSelect: footer.handleAgentSelect,
|
||||||
onModelSelect: footer.handleModelSelect,
|
onModelSelect: footer.handleModelSelect,
|
||||||
onVariantSelect: footer.handleVariantSelect,
|
onVariantSelect: footer.handleVariantSelect,
|
||||||
onRows: footer.syncRows,
|
onRows: footer.syncRows,
|
||||||
|
|
@ -377,7 +393,7 @@ export class RunFooter implements FooterApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (next.type === "agent") {
|
if (next.type === "agent") {
|
||||||
this.options.agentLabel = Locale.titlecase(next.agent ?? "build")
|
this.setCurrentAgentID(next.agent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -386,13 +402,15 @@ export class RunFooter implements FooterApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (next.type === "turn.duration") {
|
if (next.type === "turn.duration") {
|
||||||
|
const agent = this.turnAgent ?? this.currentAgent()
|
||||||
|
this.turnAgent = undefined
|
||||||
if (this.miniSettings().turn_summary === "hide") return
|
if (this.miniSettings().turn_summary === "hide") return
|
||||||
const current = this.currentModel()
|
const current = this.currentModel()
|
||||||
this.flush()
|
this.flush()
|
||||||
this.flushing = this.flushing
|
this.flushing = this.flushing
|
||||||
.then(() =>
|
.then(() =>
|
||||||
this.scrollback.writeTurnSummary({
|
this.scrollback.writeTurnSummary({
|
||||||
agent: this.options.agentLabel,
|
agent,
|
||||||
model: current ? modelInfo(this.providers(), current).model : this.state().model,
|
model: current ? modelInfo(this.providers(), current).model : this.state().model,
|
||||||
duration: next.duration,
|
duration: next.duration,
|
||||||
}),
|
}),
|
||||||
|
|
@ -451,6 +469,7 @@ export class RunFooter implements FooterApi {
|
||||||
patch.notice = ""
|
patch.notice = ""
|
||||||
}
|
}
|
||||||
if (next.type === "turn.send") {
|
if (next.type === "turn.send") {
|
||||||
|
this.turnAgent = this.currentAgent()
|
||||||
this.clearInterruptTimer()
|
this.clearInterruptTimer()
|
||||||
this.clearExitTimer()
|
this.clearExitTimer()
|
||||||
}
|
}
|
||||||
|
|
@ -667,7 +686,7 @@ export class RunFooter implements FooterApi {
|
||||||
? this.base + PERMISSION_ROWS
|
? this.base + PERMISSION_ROWS
|
||||||
: type === "form"
|
: type === "form"
|
||||||
? this.base + FORM_ROWS
|
? this.base + FORM_ROWS
|
||||||
: ["command", "skill", "model", "variant", "settings"].includes(route)
|
: ["command", "skill", "agent", "model", "variant", "settings"].includes(route)
|
||||||
? 1 + RUN_COMMAND_PANEL_ROWS
|
? 1 + RUN_COMMAND_PANEL_ROWS
|
||||||
: route === "queued-menu" || route === "subagent-menu"
|
: route === "queued-menu" || route === "subagent-menu"
|
||||||
? 1 + this.subagentMenuRows
|
? 1 + this.subagentMenuRows
|
||||||
|
|
@ -815,6 +834,13 @@ export class RunFooter implements FooterApi {
|
||||||
.catch(() => {})
|
.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 => {
|
private handleVariantSelect = (variant: string | undefined): void => {
|
||||||
if (this.isClosed) {
|
if (this.isClosed) {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { registerOpencodeSpinner } from "../component/register-spinner"
|
||||||
import { createColors, createFrames } from "../ui/spinner"
|
import { createColors, createFrames } from "../ui/spinner"
|
||||||
import {
|
import {
|
||||||
RUN_SUBAGENT_PANEL_ROWS,
|
RUN_SUBAGENT_PANEL_ROWS,
|
||||||
|
RunAgentSelectBody,
|
||||||
RunCommandMenuBody,
|
RunCommandMenuBody,
|
||||||
RunModelSelectBody,
|
RunModelSelectBody,
|
||||||
RunQueuedPromptSelectBody,
|
RunQueuedPromptSelectBody,
|
||||||
|
|
@ -76,6 +77,8 @@ type RunFooterViewProps = {
|
||||||
references: () => RunReference[]
|
references: () => RunReference[]
|
||||||
commands: () => RunCommand[] | undefined
|
commands: () => RunCommand[] | undefined
|
||||||
providers: () => RunProvider[] | undefined
|
providers: () => RunProvider[] | undefined
|
||||||
|
currentAgent: () => string
|
||||||
|
currentAgentID: () => string | undefined
|
||||||
currentModel: () => RunInput["model"]
|
currentModel: () => RunInput["model"]
|
||||||
variants: () => string[]
|
variants: () => string[]
|
||||||
currentVariant: () => string | undefined
|
currentVariant: () => string | undefined
|
||||||
|
|
@ -99,6 +102,7 @@ type RunFooterViewProps = {
|
||||||
onExitRequest?: () => boolean
|
onExitRequest?: () => boolean
|
||||||
onRequestExit?: (fn: (() => boolean) | undefined) => void
|
onRequestExit?: (fn: (() => boolean) | undefined) => void
|
||||||
onExit: () => void
|
onExit: () => void
|
||||||
|
onAgentSelect: (agent: string) => void
|
||||||
onModelSelect: (model: NonNullable<RunInput["model"]>) => void
|
onModelSelect: (model: NonNullable<RunInput["model"]>) => void
|
||||||
onVariantSelect: (variant: string | undefined) => void
|
onVariantSelect: (variant: string | undefined) => void
|
||||||
onRows: (rows: number) => void
|
onRows: (rows: number) => void
|
||||||
|
|
@ -134,6 +138,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
const inspecting = createMemo(() => active().type === "prompt" && route().type === "subagent")
|
const inspecting = createMemo(() => active().type === "prompt" && route().type === "subagent")
|
||||||
const commanding = createMemo(() => active().type === "prompt" && route().type === "command")
|
const commanding = createMemo(() => active().type === "prompt" && route().type === "command")
|
||||||
const skilling = createMemo(() => active().type === "prompt" && route().type === "skill")
|
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 modeling = createMemo(() => active().type === "prompt" && route().type === "model")
|
||||||
const varianting = createMemo(() => active().type === "prompt" && route().type === "variant")
|
const varianting = createMemo(() => active().type === "prompt" && route().type === "variant")
|
||||||
const setting = createMemo(() => active().type === "prompt" && route().type === "settings")
|
const setting = createMemo(() => active().type === "prompt" && route().type === "settings")
|
||||||
|
|
@ -145,6 +150,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
selectingSubagent() ||
|
selectingSubagent() ||
|
||||||
commanding() ||
|
commanding() ||
|
||||||
skilling() ||
|
skilling() ||
|
||||||
|
agenting() ||
|
||||||
modeling() ||
|
modeling() ||
|
||||||
varianting() ||
|
varianting() ||
|
||||||
setting(),
|
setting(),
|
||||||
|
|
@ -219,7 +225,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
const footerStatus = createMemo(() => {
|
const footerStatus = createMemo(() => {
|
||||||
const current = model() ?? props.state().model.trim()
|
const current = model() ?? props.state().model.trim()
|
||||||
const variant = props.currentVariant()
|
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 (current) details.push(variant ? `${current} ${variant}` : current)
|
||||||
if (usage()) details.push(props.mono ? usage().replaceAll(" · ", " - ") : usage())
|
if (usage()) details.push(props.mono ? usage().replaceAll(" · ", " - ") : usage())
|
||||||
if (queuedPrompts().length > 0) details.push(`${queuedPrompts().length} pending`)
|
if (queuedPrompts().length > 0) details.push(`${queuedPrompts().length} pending`)
|
||||||
|
|
@ -268,6 +274,11 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
props.onSubagentSelect?.(undefined)
|
props.onSubagentSelect?.(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openAgent = () => {
|
||||||
|
setRoute({ type: "agent" })
|
||||||
|
props.onSubagentSelect?.(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
const openSkillMenu = () => {
|
const openSkillMenu = () => {
|
||||||
if (props.commands() && skills().length === 0) {
|
if (props.commands() && skills().length === 0) {
|
||||||
return
|
return
|
||||||
|
|
@ -407,8 +418,9 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
})
|
})
|
||||||
const modelStatus = createMemo(() => {
|
const modelStatus = createMemo(() => {
|
||||||
const current = model() ?? props.state().model.trim()
|
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 {
|
return {
|
||||||
|
agent: props.currentAgent(),
|
||||||
model: current,
|
model: current,
|
||||||
variant: responsive().statusline.showModelVariant ? props.currentVariant() : undefined,
|
variant: responsive().statusline.showModelVariant ? props.currentVariant() : undefined,
|
||||||
}
|
}
|
||||||
|
|
@ -593,6 +605,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
if (
|
if (
|
||||||
current.type !== "command" &&
|
current.type !== "command" &&
|
||||||
current.type !== "skill" &&
|
current.type !== "skill" &&
|
||||||
|
current.type !== "agent" &&
|
||||||
current.type !== "model" &&
|
current.type !== "model" &&
|
||||||
current.type !== "variant" &&
|
current.type !== "variant" &&
|
||||||
current.type !== "settings" &&
|
current.type !== "settings" &&
|
||||||
|
|
@ -698,6 +711,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
variants={props.variants}
|
variants={props.variants}
|
||||||
variantCycle={variantCycle()}
|
variantCycle={variantCycle()}
|
||||||
onClose={closePanel}
|
onClose={closePanel}
|
||||||
|
onAgent={openAgent}
|
||||||
onModel={openModel}
|
onModel={openModel}
|
||||||
onEditor={() => {
|
onEditor={() => {
|
||||||
closePanel()
|
closePanel()
|
||||||
|
|
@ -748,6 +762,19 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
mono={props.mono}
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</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()}>
|
<Match when={modeling()}>
|
||||||
<RunModelSelectBody
|
<RunModelSelectBody
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|
@ -907,6 +934,10 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
flexShrink={1}
|
flexShrink={1}
|
||||||
>
|
>
|
||||||
<text fg={theme().text} wrapMode="none" truncate>
|
<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}
|
{info().model}
|
||||||
<Show when={info().variant}>
|
<Show when={info().variant}>
|
||||||
{(variant) => <span style={{ fg: theme().warning, bold: true }}> {variant()}</span>}
|
{(variant) => <span style={{ fg: theme().warning, bold: true }}> {variant()}</span>}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export function footerWidthPolicy(width: number) {
|
||||||
},
|
},
|
||||||
statusline: {
|
statusline: {
|
||||||
showActivityMeta: compact,
|
showActivityMeta: compact,
|
||||||
|
showAgent: compact,
|
||||||
showCommandHint: width >= FOOTER_WIDTH_BREAKPOINTS.commandHint,
|
showCommandHint: width >= FOOTER_WIDTH_BREAKPOINTS.commandHint,
|
||||||
showModel: width >= FOOTER_WIDTH_BREAKPOINTS.model,
|
showModel: width >= FOOTER_WIDTH_BREAKPOINTS.model,
|
||||||
showModelVariant: width >= FOOTER_WIDTH_BREAKPOINTS.modelVariant,
|
showModelVariant: width >= FOOTER_WIDTH_BREAKPOINTS.modelVariant,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { CliRenderEvents, createCliRenderer, type CliRenderer, type ScrollbackWriter } from "@opentui/core"
|
import { CliRenderEvents, createCliRenderer, type CliRenderer, type ScrollbackWriter } from "@opentui/core"
|
||||||
import { isDefaultTitle } from "../util/session"
|
import { isDefaultTitle } from "../util/session"
|
||||||
import { Locale } from "../util/locale"
|
|
||||||
import { entrySplash, exitSplash, splashMeta } from "./splash"
|
import { entrySplash, exitSplash, splashMeta } from "./splash"
|
||||||
import { resolveRunTheme } from "./theme"
|
import { resolveRunTheme } from "./theme"
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -45,11 +44,6 @@ type CycleResult = {
|
||||||
variants?: string[]
|
variants?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type FooterLabels = {
|
|
||||||
agentLabel: string
|
|
||||||
modelLabel: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LifecycleInput = {
|
export type LifecycleInput = {
|
||||||
host: MiniHost
|
host: MiniHost
|
||||||
getDirectory: () => string
|
getDirectory: () => string
|
||||||
|
|
@ -70,6 +64,7 @@ export type LifecycleInput = {
|
||||||
onFormReply: (input: FormReply) => void | Promise<void>
|
onFormReply: (input: FormReply) => void | Promise<void>
|
||||||
onFormCancel: (input: FormCancel) => void | Promise<void>
|
onFormCancel: (input: FormCancel) => void | Promise<void>
|
||||||
onCycleVariant?: () => CycleResult | void
|
onCycleVariant?: () => CycleResult | void
|
||||||
|
onAgentSelect?: (agent: string) => void
|
||||||
onModelSelect?: (model: NonNullable<RunInput["model"]>) => CycleResult | void | Promise<CycleResult | void>
|
onModelSelect?: (model: NonNullable<RunInput["model"]>) => CycleResult | void | Promise<CycleResult | void>
|
||||||
onVariantSelect?: (variant: string | undefined) => CycleResult | void | Promise<CycleResult | void>
|
onVariantSelect?: (variant: string | undefined) => CycleResult | void | Promise<CycleResult | void>
|
||||||
onInterrupt?: () => 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) {
|
function directoryLabel(directory: string, home: string) {
|
||||||
const resolved = path.resolve(directory)
|
const resolved = path.resolve(directory)
|
||||||
const display =
|
const display =
|
||||||
|
|
@ -203,11 +190,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
||||||
session_id: input.sessionID,
|
session_id: input.sessionID,
|
||||||
mono,
|
mono,
|
||||||
})
|
})
|
||||||
const labels = footerLabels({
|
|
||||||
agent: input.agent,
|
|
||||||
model: input.model,
|
|
||||||
variant: input.variant,
|
|
||||||
})
|
|
||||||
const wrote = queueSplash(
|
const wrote = queueSplash(
|
||||||
renderer,
|
renderer,
|
||||||
state,
|
state,
|
||||||
|
|
@ -232,7 +214,8 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
||||||
findFiles: input.findFiles,
|
findFiles: input.findFiles,
|
||||||
agents: input.agents,
|
agents: input.agents,
|
||||||
references: input.references,
|
references: input.references,
|
||||||
...labels,
|
agent: input.agent,
|
||||||
|
modelLabel: input.model ? formatModelLabel(input.model, input.variant) : "Default model",
|
||||||
model: input.model,
|
model: input.model,
|
||||||
variant: input.variant,
|
variant: input.variant,
|
||||||
first: input.first,
|
first: input.first,
|
||||||
|
|
@ -249,6 +232,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
||||||
onFormReply: input.onFormReply,
|
onFormReply: input.onFormReply,
|
||||||
onFormCancel: input.onFormCancel,
|
onFormCancel: input.onFormCancel,
|
||||||
onCycleVariant: input.onCycleVariant,
|
onCycleVariant: input.onCycleVariant,
|
||||||
|
onAgentSelect: input.onAgentSelect,
|
||||||
onModelSelect: input.onModelSelect,
|
onModelSelect: input.onModelSelect,
|
||||||
onVariantSelect: input.onVariantSelect,
|
onVariantSelect: input.onVariantSelect,
|
||||||
onInterrupt: input.onInterrupt,
|
onInterrupt: input.onInterrupt,
|
||||||
|
|
|
||||||
|
|
@ -304,6 +304,9 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||||
variant: state.activeVariant,
|
variant: state.activeVariant,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onAgentSelect: (agent) => {
|
||||||
|
state.agent = agent
|
||||||
|
},
|
||||||
onModelSelect: async (model) => {
|
onModelSelect: async (model) => {
|
||||||
if (state.model?.providerID === model.providerID && state.model.modelID === model.modelID) {
|
if (state.model?.providerID === model.providerID && state.model.modelID === model.modelID) {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1553,6 +1553,8 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
throw new Error("This prompt cannot be queued")
|
throw new Error("This prompt cannot be queued")
|
||||||
if (!state.connected) throw new Error("Event stream is reconnecting")
|
if (!state.connected) throw new Error("Event stream is reconnecting")
|
||||||
const client = sdk
|
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"))
|
mergePending(await admitPrompt(next, client, "queue"))
|
||||||
settlementClient = client
|
settlementClient = client
|
||||||
},
|
},
|
||||||
|
|
@ -1574,6 +1576,8 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
|
|
||||||
const command = next.prompt.command
|
const command = next.prompt.command
|
||||||
if (command?.source === "skill") {
|
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 })
|
input.trace?.write("send.skill", { sessionID: input.sessionID, messageID, skill: command.name })
|
||||||
await runTurnWait(
|
await runTurnWait(
|
||||||
next,
|
next,
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ export type FooterQueuedPrompt = {
|
||||||
export type RunAgent = {
|
export type RunAgent = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
description?: string
|
||||||
mode: "subagent" | "primary" | "all"
|
mode: "subagent" | "primary" | "all"
|
||||||
hidden: boolean
|
hidden: boolean
|
||||||
}
|
}
|
||||||
|
|
@ -286,6 +287,7 @@ export type FooterPromptRoute =
|
||||||
| { type: "subagent"; sessionID: string }
|
| { type: "subagent"; sessionID: string }
|
||||||
| { type: "command" }
|
| { type: "command" }
|
||||||
| { type: "skill" }
|
| { type: "skill" }
|
||||||
|
| { type: "agent" }
|
||||||
| { type: "model" }
|
| { type: "model" }
|
||||||
| { type: "variant" }
|
| { type: "variant" }
|
||||||
| { type: "settings" }
|
| { type: "settings" }
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ test("down opens subagents from an empty prompt", async () => {
|
||||||
references={() => []}
|
references={() => []}
|
||||||
commands={() => []}
|
commands={() => []}
|
||||||
providers={() => undefined}
|
providers={() => undefined}
|
||||||
|
currentAgent={() => "Build"}
|
||||||
|
currentAgentID={() => "build"}
|
||||||
currentModel={() => undefined}
|
currentModel={() => undefined}
|
||||||
variants={() => []}
|
variants={() => []}
|
||||||
currentVariant={() => undefined}
|
currentVariant={() => undefined}
|
||||||
|
|
@ -65,6 +67,7 @@ test("down opens subagents from an empty prompt", async () => {
|
||||||
onEditorOpen={async () => undefined}
|
onEditorOpen={async () => undefined}
|
||||||
onInputClear={() => {}}
|
onInputClear={() => {}}
|
||||||
onExit={() => {}}
|
onExit={() => {}}
|
||||||
|
onAgentSelect={() => {}}
|
||||||
onModelSelect={() => {}}
|
onModelSelect={() => {}}
|
||||||
onVariantSelect={() => {}}
|
onVariantSelect={() => {}}
|
||||||
onRows={() => {}}
|
onRows={() => {}}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { Keymap } from "../../src/context/keymap"
|
||||||
import {
|
import {
|
||||||
RUN_COMMAND_PANEL_ROWS,
|
RUN_COMMAND_PANEL_ROWS,
|
||||||
RUN_SUBAGENT_PANEL_ROWS,
|
RUN_SUBAGENT_PANEL_ROWS,
|
||||||
|
RunAgentSelectBody,
|
||||||
RunCommandMenuBody,
|
RunCommandMenuBody,
|
||||||
RunModelSelectBody,
|
RunModelSelectBody,
|
||||||
RunQueuedPromptSelectBody,
|
RunQueuedPromptSelectBody,
|
||||||
|
|
@ -26,6 +27,7 @@ import type {
|
||||||
FooterView,
|
FooterView,
|
||||||
MiniSettingChange,
|
MiniSettingChange,
|
||||||
MiniSettings,
|
MiniSettings,
|
||||||
|
RunAgent,
|
||||||
RunCommand,
|
RunCommand,
|
||||||
RunInput,
|
RunInput,
|
||||||
RunPrompt,
|
RunPrompt,
|
||||||
|
|
@ -110,6 +112,7 @@ async function renderFooter(
|
||||||
commands?: RunCommand[]
|
commands?: RunCommand[]
|
||||||
theme?: () => RunTheme
|
theme?: () => RunTheme
|
||||||
providers?: RunProvider[]
|
providers?: RunProvider[]
|
||||||
|
currentAgent?: string
|
||||||
currentModel?: RunInput["model"]
|
currentModel?: RunInput["model"]
|
||||||
currentVariant?: string
|
currentVariant?: string
|
||||||
subagents?: FooterSubagentState
|
subagents?: FooterSubagentState
|
||||||
|
|
@ -122,6 +125,7 @@ async function renderFooter(
|
||||||
onFormReply?: (input: unknown) => void
|
onFormReply?: (input: unknown) => void
|
||||||
miniSettings?: MiniSettings
|
miniSettings?: MiniSettings
|
||||||
mono?: boolean
|
mono?: boolean
|
||||||
|
onStatus?: (status: string) => void
|
||||||
onMiniSettingChange?: (change: MiniSettingChange) => void
|
onMiniSettingChange?: (change: MiniSettingChange) => void
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
|
|
@ -144,6 +148,8 @@ async function renderFooter(
|
||||||
references={() => []}
|
references={() => []}
|
||||||
commands={() => input.commands ?? []}
|
commands={() => input.commands ?? []}
|
||||||
providers={() => input.providers}
|
providers={() => input.providers}
|
||||||
|
currentAgent={() => input.currentAgent ?? "Build"}
|
||||||
|
currentAgentID={() => input.currentAgent?.toLowerCase() ?? "build"}
|
||||||
currentModel={() => input.currentModel}
|
currentModel={() => input.currentModel}
|
||||||
variants={() => []}
|
variants={() => []}
|
||||||
currentVariant={() => input.currentVariant}
|
currentVariant={() => input.currentVariant}
|
||||||
|
|
@ -162,11 +168,12 @@ async function renderFooter(
|
||||||
onEditorOpen={async () => undefined}
|
onEditorOpen={async () => undefined}
|
||||||
onInputClear={() => {}}
|
onInputClear={() => {}}
|
||||||
onExit={() => {}}
|
onExit={() => {}}
|
||||||
|
onAgentSelect={() => {}}
|
||||||
onModelSelect={() => {}}
|
onModelSelect={() => {}}
|
||||||
onVariantSelect={() => {}}
|
onVariantSelect={() => {}}
|
||||||
onRows={() => {}}
|
onRows={() => {}}
|
||||||
onLayout={() => {}}
|
onLayout={() => {}}
|
||||||
onStatus={() => {}}
|
onStatus={(status) => input.onStatus?.(status)}
|
||||||
onMiniSettingChange={(change) => input.onMiniSettingChange?.(change)}
|
onMiniSettingChange={(change) => input.onMiniSettingChange?.(change)}
|
||||||
/>
|
/>
|
||||||
</Keymap.Provider>
|
</Keymap.Provider>
|
||||||
|
|
@ -385,6 +392,7 @@ test("direct command panel renders grouped actions without catalog commands", as
|
||||||
variants={variants}
|
variants={variants}
|
||||||
variantCycle="ctrl+t"
|
variantCycle="ctrl+t"
|
||||||
onClose={() => {}}
|
onClose={() => {}}
|
||||||
|
onAgent={() => {}}
|
||||||
onModel={() => {}}
|
onModel={() => {}}
|
||||||
onEditor={() => {}}
|
onEditor={() => {}}
|
||||||
onSkill={() => {}}
|
onSkill={() => {}}
|
||||||
|
|
@ -432,6 +440,11 @@ test("direct command panel renders grouped actions without catalog commands", as
|
||||||
expect(frame).not.toContain("Review code")
|
expect(frame).not.toContain("Review code")
|
||||||
expect(frame).not.toContain("Commands 8")
|
expect(frame).not.toContain("Commands 8")
|
||||||
|
|
||||||
|
await app.mockInput.typeText("agent")
|
||||||
|
await app.renderOnce()
|
||||||
|
expect(app.captureCharFrame()).toContain("Switch agent")
|
||||||
|
|
||||||
|
app.mockInput.pressKey("u", { ctrl: true })
|
||||||
await app.mockInput.typeText("review")
|
await app.mockInput.typeText("review")
|
||||||
await app.renderOnce()
|
await app.renderOnce()
|
||||||
expect(app.captureCharFrame()).toContain("No results found")
|
expect(app.captureCharFrame()).toContain("No results found")
|
||||||
|
|
@ -615,6 +628,7 @@ test("direct command panel shows subagent entry when available", async () => {
|
||||||
variants={variants}
|
variants={variants}
|
||||||
variantCycle="ctrl+t"
|
variantCycle="ctrl+t"
|
||||||
onClose={() => {}}
|
onClose={() => {}}
|
||||||
|
onAgent={() => {}}
|
||||||
onModel={() => {}}
|
onModel={() => {}}
|
||||||
onEditor={() => {}}
|
onEditor={() => {}}
|
||||||
onSkill={() => {}}
|
onSkill={() => {}}
|
||||||
|
|
@ -665,6 +679,7 @@ test("direct command panel keeps completed subagents available", async () => {
|
||||||
variants={variants}
|
variants={variants}
|
||||||
variantCycle="ctrl+t"
|
variantCycle="ctrl+t"
|
||||||
onClose={() => {}}
|
onClose={() => {}}
|
||||||
|
onAgent={() => {}}
|
||||||
onModel={() => {}}
|
onModel={() => {}}
|
||||||
onEditor={() => {}}
|
onEditor={() => {}}
|
||||||
onSkill={() => {}}
|
onSkill={() => {}}
|
||||||
|
|
@ -1134,6 +1149,8 @@ test("direct footer shows authoritative pending work while running", async () =>
|
||||||
references={() => []}
|
references={() => []}
|
||||||
commands={() => []}
|
commands={() => []}
|
||||||
providers={() => undefined}
|
providers={() => undefined}
|
||||||
|
currentAgent={() => "Build"}
|
||||||
|
currentAgentID={() => "build"}
|
||||||
currentModel={() => ({
|
currentModel={() => ({
|
||||||
providerID: "opencode",
|
providerID: "opencode",
|
||||||
modelID: "a-model-name-long-enough-to-force-responsive-truncation",
|
modelID: "a-model-name-long-enough-to-force-responsive-truncation",
|
||||||
|
|
@ -1163,6 +1180,7 @@ test("direct footer shows authoritative pending work while running", async () =>
|
||||||
onEditorOpen={async () => undefined}
|
onEditorOpen={async () => undefined}
|
||||||
onInputClear={() => {}}
|
onInputClear={() => {}}
|
||||||
onExit={() => {}}
|
onExit={() => {}}
|
||||||
|
onAgentSelect={() => {}}
|
||||||
onModelSelect={() => {}}
|
onModelSelect={() => {}}
|
||||||
onVariantSelect={() => {}}
|
onVariantSelect={() => {}}
|
||||||
onRows={() => {}}
|
onRows={() => {}}
|
||||||
|
|
@ -1221,12 +1239,14 @@ test("direct footer shows authoritative pending work while running", async () =>
|
||||||
|
|
||||||
test("direct footer progressively adds model details after the command hint", async () => {
|
test("direct footer progressively adds model details after the command hint", async () => {
|
||||||
for (const expected of [
|
for (const expected of [
|
||||||
{ width: 24, model: false, variant: false },
|
{ width: 24, agent: false, model: false, variant: false },
|
||||||
{ width: 32, model: true, variant: false },
|
{ width: 32, agent: false, model: true, variant: false },
|
||||||
{ width: 40, model: true, variant: true },
|
{ width: 40, agent: false, model: true, variant: true },
|
||||||
|
{ width: 80, agent: true, model: true, variant: true },
|
||||||
]) {
|
]) {
|
||||||
const app = await renderFooter({
|
const app = await renderFooter({
|
||||||
providers: [provider()],
|
providers: [provider()],
|
||||||
|
currentAgent: "Plan",
|
||||||
currentModel: { providerID: "opencode", modelID: "gpt-5" },
|
currentModel: { providerID: "opencode", modelID: "gpt-5" },
|
||||||
currentVariant: "xhigh",
|
currentVariant: "xhigh",
|
||||||
width: expected.width,
|
width: expected.width,
|
||||||
|
|
@ -1238,6 +1258,7 @@ test("direct footer progressively adds model details after the command hint", as
|
||||||
expect({
|
expect({
|
||||||
width: expected.width,
|
width: expected.width,
|
||||||
command: frame.includes("ctrl+p cmd"),
|
command: frame.includes("ctrl+p cmd"),
|
||||||
|
agent: frame.includes("Plan"),
|
||||||
model: frame.includes("GPT-5"),
|
model: frame.includes("GPT-5"),
|
||||||
variant: frame.includes("xhigh"),
|
variant: frame.includes("xhigh"),
|
||||||
}).toEqual({ ...expected, command: true })
|
}).toEqual({ ...expected, command: true })
|
||||||
|
|
@ -1327,8 +1348,10 @@ test("direct footer shows full usage metadata when room is available", async ()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("direct footer hides routine activity and shows explicit notices", async () => {
|
test("direct footer hides routine activity and shows explicit notices", async () => {
|
||||||
|
let status = ""
|
||||||
const app = await renderFooter({
|
const app = await renderFooter({
|
||||||
state: { usage: "159.6K (16%) · $4.23" },
|
state: { usage: "159.6K (16%) · $4.23" },
|
||||||
|
currentAgent: "Plan",
|
||||||
miniSettings: {
|
miniSettings: {
|
||||||
thinking: "hide",
|
thinking: "hide",
|
||||||
shell_output: "hide",
|
shell_output: "hide",
|
||||||
|
|
@ -1337,6 +1360,7 @@ test("direct footer hides routine activity and shows explicit notices", async ()
|
||||||
mono: true,
|
mono: true,
|
||||||
},
|
},
|
||||||
mono: true,
|
mono: true,
|
||||||
|
onStatus: (value) => (status = value),
|
||||||
width: 160,
|
width: 160,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -1344,6 +1368,7 @@ test("direct footer hides routine activity and shows explicit notices", async ()
|
||||||
await app.renderOnce()
|
await app.renderOnce()
|
||||||
const initial = app.captureCharFrame()
|
const initial = app.captureCharFrame()
|
||||||
expect(initial).toContain("ctrl+p cmd")
|
expect(initial).toContain("ctrl+p cmd")
|
||||||
|
expect(initial).not.toContain("Plan")
|
||||||
expect(initial).not.toContain("gpt-5")
|
expect(initial).not.toContain("gpt-5")
|
||||||
expect(initial).not.toContain("159.6K")
|
expect(initial).not.toContain("159.6K")
|
||||||
|
|
||||||
|
|
@ -1359,6 +1384,12 @@ test("direct footer hides routine activity and shows explicit notices", async ()
|
||||||
expect(changed).not.toContain("159.6K")
|
expect(changed).not.toContain("159.6K")
|
||||||
expect(boxPath(statusline, "SpinnerRenderable")).toBeUndefined()
|
expect(boxPath(statusline, "SpinnerRenderable")).toBeUndefined()
|
||||||
|
|
||||||
|
app.mockInput.pressKey("p", { ctrl: true })
|
||||||
|
await app.renderOnce()
|
||||||
|
await app.mockInput.typeText("status")
|
||||||
|
app.mockInput.pressEnter()
|
||||||
|
expect(status).toBe("running - agent Plan - gpt-5 - 159.6K (16%) - $4.23")
|
||||||
|
|
||||||
app.setState((state) => ({ ...state, notice: "variant high" }))
|
app.setState((state) => ({ ...state, notice: "variant high" }))
|
||||||
await app.renderOnce()
|
await app.renderOnce()
|
||||||
expect(app.captureCharFrame()).toContain("variant high")
|
expect(app.captureCharFrame()).toContain("variant high")
|
||||||
|
|
@ -1473,6 +1504,53 @@ test("direct model panel renders current model selector", async () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("direct agent panel shows eligible agents and marks the current agent", async () => {
|
||||||
|
const [agents] = createSignal<RunAgent[]>([
|
||||||
|
{ id: "build", name: "Build", description: "Build software", mode: "all", hidden: false },
|
||||||
|
{ id: "review", name: "Review", description: "Review changes", mode: "primary", hidden: false },
|
||||||
|
{ id: "explore", name: "Explore", mode: "subagent", hidden: false },
|
||||||
|
{ id: "secret", name: "Secret", mode: "all", hidden: true },
|
||||||
|
])
|
||||||
|
const [current] = createSignal("review")
|
||||||
|
let selected: string | undefined
|
||||||
|
|
||||||
|
const app = await testRender(
|
||||||
|
() => (
|
||||||
|
<box width={100} height={RUN_COMMAND_PANEL_ROWS}>
|
||||||
|
<RunAgentSelectBody
|
||||||
|
theme={() => RUN_THEME_FALLBACK.footer}
|
||||||
|
agents={agents}
|
||||||
|
current={current}
|
||||||
|
onClose={() => {}}
|
||||||
|
onSelect={(agent) => (selected = agent)}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
width: 100,
|
||||||
|
height: RUN_COMMAND_PANEL_ROWS,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await app.renderOnce()
|
||||||
|
const frame = app.captureCharFrame()
|
||||||
|
|
||||||
|
expect(frame).toContain("Select agent")
|
||||||
|
expect(frame).toContain("build")
|
||||||
|
expect(frame).toContain("review")
|
||||||
|
expect(frame).toContain("Review changes")
|
||||||
|
expect(frame).toContain("current")
|
||||||
|
expect(frame).not.toContain("explore")
|
||||||
|
expect(frame).not.toContain("secret")
|
||||||
|
|
||||||
|
app.mockInput.pressEnter()
|
||||||
|
expect(selected).toBe("review")
|
||||||
|
} finally {
|
||||||
|
app.renderer.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("direct variant panel renders current variant selector", async () => {
|
test("direct variant panel renders current variant selector", async () => {
|
||||||
const [variants] = createSignal(["high", "minimal"])
|
const [variants] = createSignal(["high", "minimal"])
|
||||||
const [current] = createSignal<string | undefined>("high")
|
const [current] = createSignal<string | undefined>("high")
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,14 @@ describe("run footer width", () => {
|
||||||
const narrow = footerWidthPolicy(79)
|
const narrow = footerWidthPolicy(79)
|
||||||
expect(narrow.dialog.narrow).toBe(true)
|
expect(narrow.dialog.narrow).toBe(true)
|
||||||
expect(narrow.statusline.showActivityMeta).toBe(false)
|
expect(narrow.statusline.showActivityMeta).toBe(false)
|
||||||
|
expect(narrow.statusline.showAgent).toBe(false)
|
||||||
expect(narrow.statusline.showContextHints).toBe(false)
|
expect(narrow.statusline.showContextHints).toBe(false)
|
||||||
expect(narrow.statusline.contextHintLimit).toBe(0)
|
expect(narrow.statusline.contextHintLimit).toBe(0)
|
||||||
|
|
||||||
const compact = footerWidthPolicy(80)
|
const compact = footerWidthPolicy(80)
|
||||||
expect(compact.dialog.narrow).toBe(false)
|
expect(compact.dialog.narrow).toBe(false)
|
||||||
expect(compact.statusline.showActivityMeta).toBe(true)
|
expect(compact.statusline.showActivityMeta).toBe(true)
|
||||||
|
expect(compact.statusline.showAgent).toBe(true)
|
||||||
expect(compact.statusline.showContextHints).toBe(true)
|
expect(compact.statusline.showContextHints).toBe(true)
|
||||||
expect(compact.statusline.contextHintLimit).toBe(1)
|
expect(compact.statusline.contextHintLimit).toBe(1)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ describe("run interactive runtime", () => {
|
||||||
variants: ["low", "high"],
|
variants: ["low", "high"],
|
||||||
})
|
})
|
||||||
let lifecycle!: LifecycleInput
|
let lifecycle!: LifecycleInput
|
||||||
|
let turnAgent: string | undefined
|
||||||
let turnModel: { providerID: string; modelID: string } | undefined
|
let turnModel: { providerID: string; modelID: string } | undefined
|
||||||
let refreshCatalog: (() => Promise<unknown>) | undefined
|
let refreshCatalog: (() => Promise<unknown>) | undefined
|
||||||
stubCatalogLists(sdk, {
|
stubCatalogLists(sdk, {
|
||||||
|
|
@ -107,6 +108,7 @@ describe("run interactive runtime", () => {
|
||||||
catalogLoaded.resolve()
|
catalogLoaded.resolve()
|
||||||
return {
|
return {
|
||||||
runPromptTurn: async (input) => {
|
runPromptTurn: async (input) => {
|
||||||
|
turnAgent = input.agent
|
||||||
turnModel = input.model
|
turnModel = input.model
|
||||||
api.close()
|
api.close()
|
||||||
},
|
},
|
||||||
|
|
@ -139,8 +141,10 @@ describe("run interactive runtime", () => {
|
||||||
selection: { providerID: "test", modelID: "resolved" },
|
selection: { providerID: "test", modelID: "resolved" },
|
||||||
})
|
})
|
||||||
expect(lifecycle.onCycleVariant?.()).toMatchObject({ status: "variant low", variant: "low" })
|
expect(lifecycle.onCycleVariant?.()).toMatchObject({ status: "variant low", variant: "low" })
|
||||||
|
lifecycle.onAgentSelect?.("review")
|
||||||
ui.submit("hello")
|
ui.submit("hello")
|
||||||
while (!turnModel) await Bun.sleep(0)
|
while (!turnModel) await Bun.sleep(0)
|
||||||
|
expect(turnAgent).toBe("review")
|
||||||
expect(turnModel).toEqual({ providerID: "test", modelID: "resolved" })
|
expect(turnModel).toEqual({ providerID: "test", modelID: "resolved" })
|
||||||
await task
|
await task
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -624,13 +624,17 @@ describe("V2 mini transport", () => {
|
||||||
ok({ ...promptAdmission(request), admittedSeq: 2 }) as never,
|
ok({ ...promptAdmission(request), admittedSeq: 2 }) as never,
|
||||||
)
|
)
|
||||||
await transport.queuePromptTurn({
|
await transport.queuePromptTurn({
|
||||||
agent: undefined,
|
agent: "review",
|
||||||
model: undefined,
|
model: undefined,
|
||||||
variant: undefined,
|
variant: undefined,
|
||||||
prompt: { messageID: "msg_next", text: "another", parts: [] },
|
prompt: { messageID: "msg_next", text: "another", parts: [] },
|
||||||
files: [],
|
files: [],
|
||||||
includeFiles: false,
|
includeFiles: false,
|
||||||
})
|
})
|
||||||
|
expect(client.session.switchAgent).toHaveBeenCalledWith(
|
||||||
|
{ sessionID: "ses_1", agent: "review" },
|
||||||
|
expect.anything(),
|
||||||
|
)
|
||||||
expect(prompt).toHaveBeenCalledWith(expect.objectContaining({ delivery: "queue" }), expect.anything())
|
expect(prompt).toHaveBeenCalledWith(expect.objectContaining({ delivery: "queue" }), expect.anything())
|
||||||
events.push({
|
events.push({
|
||||||
id: "evt_earlier_admission",
|
id: "evt_earlier_admission",
|
||||||
|
|
@ -2714,7 +2718,7 @@ describe("V2 mini transport", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
await transport.runPromptTurn({
|
await transport.runPromptTurn({
|
||||||
agent: undefined,
|
agent: "review",
|
||||||
model: undefined,
|
model: undefined,
|
||||||
variant: undefined,
|
variant: undefined,
|
||||||
prompt: {
|
prompt: {
|
||||||
|
|
@ -2727,6 +2731,10 @@ describe("V2 mini transport", () => {
|
||||||
includeFiles: true,
|
includeFiles: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
expect(client.session.switchAgent).toHaveBeenCalledWith(
|
||||||
|
{ sessionID: "ses_1", agent: "review" },
|
||||||
|
expect.anything(),
|
||||||
|
)
|
||||||
expect(request).toMatchObject({ sessionID: "ses_1", id: "msg_skill", skill: "tigerstyle" })
|
expect(request).toMatchObject({ sessionID: "ses_1", id: "msg_skill", skill: "tigerstyle" })
|
||||||
expect(command).not.toHaveBeenCalled()
|
expect(command).not.toHaveBeenCalled()
|
||||||
expect(prompt).not.toHaveBeenCalled()
|
expect(prompt).not.toHaveBeenCalled()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue