fix(run): align mini with current session contracts (#35354)
This commit is contained in:
@@ -174,6 +174,10 @@ export async function resolveModelInfo(
|
||||
return runtime.runPromise((svc) => svc.resolveModelInfo(sdk, directory, model)).catch(() => emptyModelInfo())
|
||||
}
|
||||
|
||||
export function resolveModelInfoStrict(sdk: RunInput["sdk"], directory: string, model: RunInput["model"]) {
|
||||
return runtime.runPromise((svc) => svc.resolveModelInfo(sdk, directory, model))
|
||||
}
|
||||
|
||||
// Fetches session messages to determine if this is the first turn and build prompt history.
|
||||
export async function resolveSessionInfo(
|
||||
sdk: RunInput["sdk"],
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { MessageID } from "@/session/schema"
|
||||
import { loadRunAgents, loadRunCommands, loadRunReferences } from "./catalog.shared"
|
||||
import { createRunDemo } from "./demo"
|
||||
import { resolveModelInfo, resolveRunTuiConfig, resolveSessionInfo } from "./runtime.boot"
|
||||
import { resolveModelInfo, resolveModelInfoStrict, resolveRunTuiConfig, resolveSessionInfo } from "./runtime.boot"
|
||||
import { createRuntimeLifecycle } from "./runtime.lifecycle"
|
||||
import { trace } from "./trace"
|
||||
import { cycleVariant, formatModelLabel, resolveSavedVariant, resolveVariant, saveVariant } from "./variant.shared"
|
||||
@@ -378,32 +378,89 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||
state.localRows = [...state.localRows, { commit, after }].slice(-LOCAL_REPLAY_ROW_LIMIT)
|
||||
}
|
||||
|
||||
const loadCatalog = async (): Promise<void> => {
|
||||
const applyCatalog = (catalog: {
|
||||
agents: Awaited<ReturnType<typeof loadRunAgents>>
|
||||
references: Awaited<ReturnType<typeof loadRunReferences>>
|
||||
commands: Awaited<ReturnType<typeof loadRunCommands>>
|
||||
}) => {
|
||||
if (footer.isClosed) {
|
||||
return
|
||||
}
|
||||
|
||||
const [agents, references, commands] = await Promise.all([
|
||||
loadRunAgents(ctx.sdk, ctx.directory).catch(() => []),
|
||||
loadRunReferences(ctx.sdk, ctx.directory).catch(() => []),
|
||||
loadRunCommands(ctx.sdk, ctx.directory).catch(() => []),
|
||||
])
|
||||
if (footer.isClosed) {
|
||||
return
|
||||
}
|
||||
|
||||
footer.event({
|
||||
type: "catalog",
|
||||
agents,
|
||||
references,
|
||||
commands,
|
||||
agents: catalog.agents,
|
||||
references: catalog.references,
|
||||
commands: catalog.commands,
|
||||
})
|
||||
}
|
||||
|
||||
void footer
|
||||
const fetchCatalog = async () => {
|
||||
const [agents, references, commands] = await Promise.all([
|
||||
loadRunAgents(ctx.sdk, ctx.directory),
|
||||
loadRunReferences(ctx.sdk, ctx.directory),
|
||||
loadRunCommands(ctx.sdk, ctx.directory),
|
||||
])
|
||||
return { agents, references, commands }
|
||||
}
|
||||
|
||||
const loadCatalog = async () => {
|
||||
applyCatalog(
|
||||
await Promise.all([
|
||||
loadRunAgents(ctx.sdk, ctx.directory).catch(() => []),
|
||||
loadRunReferences(ctx.sdk, ctx.directory).catch(() => []),
|
||||
loadRunCommands(ctx.sdk, ctx.directory).catch(() => []),
|
||||
]).then(([agents, references, commands]) => ({ agents, references, commands })),
|
||||
)
|
||||
}
|
||||
|
||||
const applyModelInfo = (
|
||||
info: Awaited<ReturnType<typeof resolveModelInfo>>,
|
||||
current: string | undefined,
|
||||
boot = false,
|
||||
) => {
|
||||
state.providers = info.providers
|
||||
state.variants = variantsFor(state.providers, state.model)
|
||||
state.limits = info.limits
|
||||
state.activeVariant = boot
|
||||
? resolveVariant(ctx.variant, current, savedVariant, state.variants)
|
||||
: current && !state.variants.includes(current)
|
||||
? undefined
|
||||
: current
|
||||
if (footer.isClosed) return
|
||||
footer.event({ type: "models", providers: info.providers })
|
||||
footer.event({ type: "variants", variants: state.variants, current: state.activeVariant })
|
||||
if (state.model)
|
||||
footer.event({ type: "model", model: formatModelLabel(state.model, state.activeVariant, state.providers) })
|
||||
}
|
||||
|
||||
let catalogRefresh: Promise<void> | undefined
|
||||
let catalogRefreshQueued = false
|
||||
const requestCatalogRefresh = () => {
|
||||
catalogRefreshQueued = true
|
||||
if (catalogRefresh || footer.isClosed) return
|
||||
catalogRefresh = (async () => {
|
||||
await Promise.all([modelTask, initialCatalog])
|
||||
while (catalogRefreshQueued && !footer.isClosed) {
|
||||
catalogRefreshQueued = false
|
||||
const [catalog, info] = await Promise.allSettled([
|
||||
fetchCatalog(),
|
||||
resolveModelInfoStrict(ctx.sdk, ctx.directory, state.model),
|
||||
])
|
||||
if (catalog.status === "fulfilled") applyCatalog(catalog.value)
|
||||
if (info.status === "fulfilled") applyModelInfo(info.value, state.activeVariant)
|
||||
}
|
||||
})().finally(() => {
|
||||
catalogRefresh = undefined
|
||||
if (catalogRefreshQueued) requestCatalogRefresh()
|
||||
})
|
||||
void catalogRefresh.catch(() => {})
|
||||
}
|
||||
|
||||
const initialCatalog = footer
|
||||
.idle()
|
||||
.then(loadCatalog)
|
||||
.catch(() => {})
|
||||
void initialCatalog
|
||||
|
||||
if (Flag.OPENCODE_SHOW_TTFD) {
|
||||
footer.append({
|
||||
@@ -428,31 +485,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||
void Promise.resolve(input.afterPaint(ctx)).catch(() => {})
|
||||
}
|
||||
|
||||
void modelTask.then((info) => {
|
||||
state.providers = info.providers
|
||||
state.variants = variantsFor(state.providers, state.model)
|
||||
state.limits = info.limits
|
||||
|
||||
const next = resolveVariant(ctx.variant, session.variant, savedVariant, state.variants)
|
||||
if (next !== state.activeVariant) {
|
||||
state.activeVariant = next
|
||||
}
|
||||
|
||||
if (footer.isClosed) {
|
||||
return
|
||||
}
|
||||
|
||||
footer.event({ type: "models", providers: info.providers })
|
||||
footer.event({ type: "variants", variants: state.variants, current: state.activeVariant })
|
||||
if (!state.model) {
|
||||
return
|
||||
}
|
||||
|
||||
footer.event({
|
||||
type: "model",
|
||||
model: formatModelLabel(state.model, state.activeVariant, state.providers),
|
||||
})
|
||||
})
|
||||
void modelTask.then((info) => applyModelInfo(info, session.variant, true))
|
||||
|
||||
const streamTask = deps.streamTransport ?? import("./stream-v2.transport")
|
||||
const ensureStream = () => {
|
||||
@@ -484,6 +517,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||
providers: () => state.providers,
|
||||
footer,
|
||||
trace: log,
|
||||
onCatalogRefresh: requestCatalogRefresh,
|
||||
})
|
||||
if (footer.isClosed) {
|
||||
await handle.close()
|
||||
|
||||
@@ -27,7 +27,7 @@ import type { FooterSubagentDetail, FooterSubagentState, FooterSubagentTab, Stre
|
||||
|
||||
const CHILD_MESSAGE_LIMIT = 80
|
||||
const CHILD_FRAME_LIMIT = 80
|
||||
const DISCOVERY_BUFFER_LIMIT = 64
|
||||
const CHILD_EVENT_BUFFER_LIMIT = 64
|
||||
const FAMILY_LIST_LIMIT = 100
|
||||
const FALLBACK_LABEL = "Subagent"
|
||||
|
||||
@@ -160,6 +160,7 @@ type ChildState = {
|
||||
tools: Map<string, ToolTrack>
|
||||
finishedTools: Set<string>
|
||||
messageIDs: Set<string>
|
||||
prompts: Map<string, string>
|
||||
hydrated: boolean
|
||||
}
|
||||
|
||||
@@ -223,6 +224,8 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
// Foreign events buffered while a session.get discovery is in flight, so a
|
||||
// fast child (including its settled event) is not lost mid-discovery.
|
||||
const pendingEvents = new Map<string, V2Event[]>()
|
||||
const hydrationEvents = new Map<string, V2Event[]>()
|
||||
const hydrationOverflow = new Set<string>()
|
||||
const hydrations = new Map<string, Promise<void>>()
|
||||
let selected: string | undefined
|
||||
|
||||
@@ -244,6 +247,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
tools: new Map(),
|
||||
finishedTools: new Set(),
|
||||
messageIDs: new Set(),
|
||||
prompts: new Map(),
|
||||
hydrated: false,
|
||||
}
|
||||
if (!existing) children.set(sessionID, child)
|
||||
@@ -332,6 +336,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
child.callIDs.clear()
|
||||
for (const message of messages) {
|
||||
if (message.type === "user") {
|
||||
child.prompts.delete(message.id)
|
||||
userFrame(child, message.id, message.text)
|
||||
continue
|
||||
}
|
||||
@@ -382,16 +387,38 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
const hydrateChild = (child: ChildState): Promise<void> => {
|
||||
const existing = hydrations.get(child.sessionID)
|
||||
if (existing) return existing
|
||||
const pendingPrompts = new Map(child.prompts)
|
||||
const pendingTools = new Map(child.tools)
|
||||
let retry = false
|
||||
const task = input.sdk.v2.session
|
||||
.messages({ sessionID: child.sessionID, limit: CHILD_MESSAGE_LIMIT, order: "desc" }, { throwOnError: true })
|
||||
.then((response) => {
|
||||
const buffered = hydrationEvents.get(child.sessionID) ?? []
|
||||
hydrationEvents.delete(child.sessionID)
|
||||
if (hydrationOverflow.delete(child.sessionID)) {
|
||||
child.hydrated = false
|
||||
retry = true
|
||||
notifyDetail(child)
|
||||
return
|
||||
}
|
||||
for (const [id, prompt] of pendingPrompts) {
|
||||
if (!child.prompts.has(id)) child.prompts.set(id, prompt)
|
||||
}
|
||||
rebuild(child, response.data.data.toReversed())
|
||||
for (const [id, tool] of pendingTools) {
|
||||
if (!child.finishedTools.has(id) && !child.tools.has(id)) child.tools.set(id, tool)
|
||||
}
|
||||
for (const event of buffered) reduce(child, event)
|
||||
child.hydrated = true
|
||||
notifyDetail(child)
|
||||
})
|
||||
.catch(() => {})
|
||||
.catch(() => {
|
||||
hydrationEvents.delete(child.sessionID)
|
||||
hydrationOverflow.delete(child.sessionID)
|
||||
})
|
||||
.finally(() => {
|
||||
hydrations.delete(child.sessionID)
|
||||
if (retry) queueMicrotask(() => void hydrateChild(child))
|
||||
})
|
||||
hydrations.set(child.sessionID, task)
|
||||
return task
|
||||
@@ -424,8 +451,14 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
}
|
||||
|
||||
const reduce = (child: ChildState, event: V2Event) => {
|
||||
if (event.type === "session.prompt.admitted") {
|
||||
child.prompts.set(event.data.inputID, event.data.prompt.text)
|
||||
return
|
||||
}
|
||||
if (event.type === "session.prompt.promoted") {
|
||||
if (userFrame(child, event.data.inputID, "")) {
|
||||
const prompt = child.prompts.get(event.data.inputID) ?? ""
|
||||
child.prompts.delete(event.data.inputID)
|
||||
if (userFrame(child, event.data.inputID, prompt)) {
|
||||
touch(child, event.created)
|
||||
notifyDetail(child)
|
||||
}
|
||||
@@ -511,10 +544,12 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
return
|
||||
}
|
||||
if (event.type === "session.tool.input.started") {
|
||||
if (child.finishedTools.has(event.data.callID)) return
|
||||
child.tools.set(event.data.callID, { name: event.data.name, input: {}, started: event.created })
|
||||
return
|
||||
}
|
||||
if (event.type === "session.tool.called") {
|
||||
if (child.finishedTools.has(event.data.callID)) return
|
||||
const current = child.tools.get(event.data.callID)
|
||||
child.tools.set(event.data.callID, {
|
||||
name: event.data.tool,
|
||||
@@ -640,12 +675,18 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||
foreign(sessionID, event) {
|
||||
const child = children.get(sessionID)
|
||||
if (child) {
|
||||
if (hydrations.has(sessionID)) {
|
||||
const buffered = hydrationEvents.get(sessionID) ?? []
|
||||
if (buffered.length < CHILD_EVENT_BUFFER_LIMIT) buffered.push(event)
|
||||
else hydrationOverflow.add(sessionID)
|
||||
hydrationEvents.set(sessionID, buffered)
|
||||
}
|
||||
reduce(child, event)
|
||||
return
|
||||
}
|
||||
discover(sessionID)
|
||||
const buffered = pendingEvents.get(sessionID)
|
||||
if (buffered && buffered.length < DISCOVERY_BUFFER_LIMIT) buffered.push(event)
|
||||
if (buffered && buffered.length < CHILD_EVENT_BUFFER_LIMIT) buffered.push(event)
|
||||
},
|
||||
async hydrate(next) {
|
||||
for (const message of next.messages) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
SessionMessageAssistantTool,
|
||||
V2Event,
|
||||
} from "@opencode-ai/sdk/v2"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import { blockerStatus, pickBlockerView } from "./session-data"
|
||||
import { writeSessionOutput } from "./stream"
|
||||
import { createSubagentTracker, legacyTool, toolCommit } from "./stream-v2.subagent"
|
||||
@@ -41,6 +42,7 @@ type StreamInput = {
|
||||
footer: FooterApi
|
||||
trace?: Trace
|
||||
signal?: AbortSignal
|
||||
onCatalogRefresh?: () => void
|
||||
}
|
||||
|
||||
export type SessionTurnInput = {
|
||||
@@ -81,6 +83,8 @@ type Wait = {
|
||||
// callID correlates the live shell events once shell.started is observed, and
|
||||
// abort cancels the blocking request when the user interrupts the turn.
|
||||
type ShellWait = {
|
||||
eventID: string
|
||||
messageID: string
|
||||
callID?: string
|
||||
resolve: () => void
|
||||
abort: () => void
|
||||
@@ -232,7 +236,7 @@ function streamPartKey(messageID: string, partID: string) {
|
||||
function shellCommit(
|
||||
callID: string,
|
||||
command: string,
|
||||
next: { text: string; phase: "start" | "progress"; toolState: "running" | "completed" },
|
||||
next: Pick<StreamCommit, "text" | "phase" | "toolState" | "toolError">,
|
||||
): StreamCommit {
|
||||
return {
|
||||
kind: "tool",
|
||||
@@ -244,6 +248,41 @@ function shellCommit(
|
||||
}
|
||||
}
|
||||
|
||||
function shellTerminal(
|
||||
callID: string,
|
||||
command: string,
|
||||
shell: { status: string; exit?: number | string },
|
||||
output: { output: string; cursor: number; size: number; truncated: boolean },
|
||||
) {
|
||||
const incomplete = output.truncated || output.cursor < output.size
|
||||
const text = `${output.output}${incomplete ? `${output.output.endsWith("\n") || !output.output ? "" : "\n"}[output truncated]` : ""}`
|
||||
const error =
|
||||
shell.status === "exited" && shell.exit === 0
|
||||
? undefined
|
||||
: shell.status === "exited"
|
||||
? `Shell exited with code ${shell.exit ?? "unknown"}`
|
||||
: `Shell ${shell.status}`
|
||||
if (!error)
|
||||
return [shellCommit(callID, command, { text, phase: "progress", toolState: "completed" })]
|
||||
return [
|
||||
...(text ? [shellCommit(callID, command, { text, phase: "progress", toolState: "running" })] : []),
|
||||
shellCommit(callID, command, { text: error, phase: "final", toolState: "error", toolError: error }),
|
||||
]
|
||||
}
|
||||
|
||||
function messageIDFromEvent(id: string) {
|
||||
return id.replace(/^evt_/, "msg_")
|
||||
}
|
||||
|
||||
const catalogEvents = new Set([
|
||||
"catalog.updated",
|
||||
"integration.updated",
|
||||
"agent.updated",
|
||||
"command.updated",
|
||||
"skill.updated",
|
||||
"reference.updated",
|
||||
])
|
||||
|
||||
// session.shell resolves after the command settled server-side; the matching
|
||||
// live shell.ended event usually lands within the same tick, but hold the turn
|
||||
// briefly so the output commit renders inside it.
|
||||
@@ -407,6 +446,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
}
|
||||
if (message.type === "shell") {
|
||||
state.shellCommands.set(message.shell.id, message.shell.command)
|
||||
if (state.shellWait?.messageID === message.id) state.shellWait.callID = message.shell.id
|
||||
const completed = message.time.completed !== undefined
|
||||
if (!render) {
|
||||
// Suppressed history: mark settled shells rendered so live redelivery
|
||||
@@ -430,13 +470,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
}
|
||||
if (completed && message.output && !state.shellEnded.has(message.shell.id)) {
|
||||
state.shellEnded.add(message.shell.id)
|
||||
write([
|
||||
shellCommit(message.shell.id, message.shell.command, {
|
||||
text: message.output.output,
|
||||
phase: "progress",
|
||||
toolState: "completed",
|
||||
}),
|
||||
])
|
||||
write(shellTerminal(message.shell.id, message.shell.command, message.shell, message.output))
|
||||
}
|
||||
if (completed && state.shellWait?.callID === message.shell.id) state.shellWait.resolve()
|
||||
return
|
||||
@@ -522,6 +556,11 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
}
|
||||
|
||||
const apply = (event: RunV2Event) => {
|
||||
if (catalogEvents.has(event.type)) {
|
||||
if (input.directory && event.location?.directory && event.location.directory !== input.directory) return
|
||||
input.onCatalogRefresh?.()
|
||||
return
|
||||
}
|
||||
const source = sessionID(event)
|
||||
if (source !== input.sessionID) {
|
||||
if (source) subagents.foreign(source, event)
|
||||
@@ -540,8 +579,8 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
return
|
||||
}
|
||||
if (event.type === "session.skill.activated") {
|
||||
const messageID = event.id.replace(/^evt_/, "msg_")
|
||||
if (state.wait) state.wait.promoted = true
|
||||
const messageID = messageIDFromEvent(event.id)
|
||||
if (state.wait?.messageID === messageID) state.wait.promoted = true
|
||||
if (state.skillMessages.has(messageID)) return
|
||||
state.skillMessages.add(messageID)
|
||||
write([skillCommit(messageID, event.data.name)])
|
||||
@@ -550,7 +589,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
if (event.type === "session.shell.started") {
|
||||
state.shellCommands.set(event.data.shell.id, event.data.shell.command)
|
||||
const wait = state.shellWait
|
||||
if (wait && wait.callID === undefined) wait.callID = event.data.shell.id
|
||||
if (wait?.eventID === event.id) wait.callID = event.data.shell.id
|
||||
if (state.shellStarted.has(event.data.shell.id)) return
|
||||
state.shellStarted.add(event.data.shell.id)
|
||||
write(
|
||||
@@ -580,19 +619,11 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
}
|
||||
if (!state.shellEnded.has(event.data.shell.id)) {
|
||||
state.shellEnded.add(event.data.shell.id)
|
||||
commits.push(
|
||||
shellCommit(event.data.shell.id, command, {
|
||||
text: event.data.output.output,
|
||||
phase: "progress",
|
||||
toolState: "completed",
|
||||
}),
|
||||
)
|
||||
commits.push(...shellTerminal(event.data.shell.id, command, event.data.shell, event.data.output))
|
||||
}
|
||||
const wait = state.shellWait
|
||||
// An unset callID means shell.started has not been observed yet (event
|
||||
// delivery lag); mini serializes its own shells, so adopt this ended.
|
||||
const owned = wait !== undefined && (wait.callID === undefined || wait.callID === event.data.shell.id)
|
||||
write(commits, owned || state.wait ? undefined : { phase: "idle", status: "" })
|
||||
const owned = wait?.callID === event.data.shell.id
|
||||
write(commits, owned || state.wait || state.shellWait ? undefined : { phase: "idle", status: "" })
|
||||
if (owned) wait.resolve()
|
||||
return
|
||||
}
|
||||
@@ -828,6 +859,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
})()
|
||||
void consume.catch(() => {})
|
||||
await hydrate({ render: state.initial ? input.replay === true : true, reuseVisibleWait: !state.initial })
|
||||
input.onCatalogRefresh?.()
|
||||
state.initial = false
|
||||
booting = false
|
||||
for (const event of buffered.splice(0)) apply(event)
|
||||
@@ -867,13 +899,19 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
const output = new Promise<void>((resolve) => {
|
||||
rendered = resolve
|
||||
})
|
||||
const active: ShellWait = { resolve: rendered, abort: () => abort.abort() }
|
||||
const eventID = Event.ID.create()
|
||||
const active: ShellWait = {
|
||||
eventID,
|
||||
messageID: messageIDFromEvent(eventID),
|
||||
resolve: rendered,
|
||||
abort: () => abort.abort(),
|
||||
}
|
||||
state.shellWait = active
|
||||
input.trace?.write("send.shell", { sessionID: input.sessionID, command: next.prompt.text })
|
||||
input.trace?.write("send.shell", { sessionID: input.sessionID, id: eventID, command: next.prompt.text })
|
||||
write([], { phase: "running", status: "running shell" })
|
||||
try {
|
||||
await input.sdk.v2.session.shell(
|
||||
{ sessionID: input.sessionID, command: next.prompt.text },
|
||||
{ sessionID: input.sessionID, id: eventID, command: next.prompt.text },
|
||||
{ throwOnError: true, signal: abort.signal },
|
||||
)
|
||||
await Promise.race([output, wait(SHELL_OUTPUT_GRACE_MS, abort.signal)])
|
||||
|
||||
@@ -671,6 +671,10 @@ function scrollBashProgress(p: ToolProps<typeof BashTool>): string {
|
||||
}
|
||||
|
||||
function scrollBashFinal(p: ToolProps<typeof BashTool>): string {
|
||||
if (p.frame.status === "error") {
|
||||
return fail(p.frame)
|
||||
}
|
||||
|
||||
const code = p.metadata.exit ?? num(p.frame.meta.exitCode) ?? num(p.frame.meta.exit_code)
|
||||
const time = span(p.frame.state)
|
||||
if (code === undefined) {
|
||||
@@ -1427,6 +1431,11 @@ export function toolEntryBody(commit: StreamCommit, raw: string): RunEntryBody |
|
||||
return textBody(shellOutput(commit.shell.command, raw) ?? "")
|
||||
}
|
||||
|
||||
if (commit.toolState === "error") {
|
||||
const ctx = toolFrame(commit, raw)
|
||||
return textBody(toolScroll("final", ctx))
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user