chore: merge v2
This commit is contained in:
commit
3dffa01054
164 changed files with 5994 additions and 2957 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { TextAttributes } from "@opentui/core"
|
||||
import { TextAttributes, type Renderable } from "@opentui/core"
|
||||
import { TimeToFirstDraw, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||
import { open } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
|
|
@ -33,6 +33,7 @@ export function DevToolsBar() {
|
|||
const plugins = usePlugin()
|
||||
const theme = useTheme()
|
||||
const keymap = Keymap.use()
|
||||
const renderer = useRenderer()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const { themeV2, mode, supports, setMode } = theme
|
||||
const elevatedTheme = theme.contextual("elevated").themeV2
|
||||
|
|
@ -41,6 +42,7 @@ export function DevToolsBar() {
|
|||
const [dumpPath, setDumpPath] = createSignal<string>()
|
||||
const [dumpError, setDumpError] = createSignal<string>()
|
||||
const [frontendSamples, setFrontendSamples] = createSignal<readonly ProcessSample[]>([])
|
||||
let focus: Renderable | null
|
||||
const connected = createMemo(() => client.connection.status() === "connected")
|
||||
const serverIndicator = createMemo(() => connectionIndicator(client.connection.status(), client.connection.attempt()))
|
||||
const themePerformance = createMemo(
|
||||
|
|
@ -54,7 +56,22 @@ export function DevToolsBar() {
|
|||
address: info.urls[0] ? new URL(info.urls[0]).host : "Unknown",
|
||||
}
|
||||
})
|
||||
const toggle = (next: Panel) => setPanel((current) => (current === next ? undefined : next))
|
||||
const close = () => {
|
||||
setPanel()
|
||||
setTimeout(() => {
|
||||
if (panel() || !focus || focus.isDestroyed) return
|
||||
focus.focus()
|
||||
focus = null
|
||||
}, 1)
|
||||
}
|
||||
const toggle = (next: Panel) => {
|
||||
if (panel() === next) return close()
|
||||
if (!panel()) {
|
||||
focus = renderer.currentFocusedRenderable
|
||||
focus?.blur()
|
||||
}
|
||||
setPanel(next)
|
||||
}
|
||||
const nextMode = () => (mode() === "dark" ? "light" : "dark")
|
||||
const canSwitchMode = () => supports(nextMode())
|
||||
const runtime = createMemo(() => runtimeStatus(frontendSamples()))
|
||||
|
|
@ -67,7 +84,7 @@ export function DevToolsBar() {
|
|||
if (!panel() || event.name !== "escape") return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setPanel()
|
||||
close()
|
||||
},
|
||||
{ priority: 10 },
|
||||
)
|
||||
|
|
@ -205,7 +222,7 @@ export function DevToolsBar() {
|
|||
width={dimensions().width}
|
||||
height={Math.max(0, dimensions().height - 1)}
|
||||
backgroundColor="transparent"
|
||||
onMouseUp={() => setPanel()}
|
||||
onMouseUp={close}
|
||||
/>
|
||||
</Show>
|
||||
<BarItem active={panel() === "server"} onClick={() => toggle("server")}>
|
||||
|
|
|
|||
|
|
@ -222,6 +222,14 @@ const settings: Setting[] = [
|
|||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
},
|
||||
{
|
||||
title: "Turn token usage",
|
||||
category: "Debug",
|
||||
path: ["debug", "turn_tokens"],
|
||||
default: false,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
},
|
||||
]
|
||||
|
||||
export function DialogConfig() {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import type { Plugin } from "@opencode-ai/plugin/v2/tui"
|
|||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { useClient } from "./client"
|
||||
import { nonEmptyToolContent } from "../util/tool-display"
|
||||
import { createEffect, createSignal, onCleanup } from "solid-js"
|
||||
|
||||
export type DataSessionStatus = "idle" | "running"
|
||||
|
|
@ -298,6 +299,12 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
case "session.created":
|
||||
result.session.invalidate(event.data.sessionID)
|
||||
void result.session.sync(event.data.sessionID)
|
||||
// Band-aid: a newly created session starts empty, so live events can be its source of truth.
|
||||
// Fetching pending inputs and projected messages separately lets promotion move an input between snapshots,
|
||||
// causing both requests to miss it and overwrite event-built state. Skip those racy initial reads until
|
||||
// hydration can load pending and projected messages atomically.
|
||||
sync.complete(`session.pending:${event.data.sessionID}`)
|
||||
sync.complete(`session.message:${event.data.sessionID}`)
|
||||
break
|
||||
case "session.deleted":
|
||||
removeSession(event.data.sessionID)
|
||||
|
|
@ -599,7 +606,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
match.time.ran = event.created
|
||||
match.executed = event.data.executed
|
||||
match.providerState = event.data.state
|
||||
match.state = { status: "running", input: event.data.input, structured: {}, content: [] }
|
||||
match.state = { status: "running", input: event.data.input, metadata: {} }
|
||||
})
|
||||
break
|
||||
case "session.tool.progress":
|
||||
|
|
@ -609,8 +616,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
event.data.callID,
|
||||
)
|
||||
if (match?.state.status !== "running") return
|
||||
match.state.structured = event.data.structured
|
||||
match.state.content = [...event.data.content]
|
||||
match.state.metadata = event.data.metadata
|
||||
})
|
||||
break
|
||||
case "session.tool.success":
|
||||
|
|
@ -623,9 +629,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
match.state = {
|
||||
status: "completed",
|
||||
input: match.state.input,
|
||||
structured: event.data.structured,
|
||||
metadata: event.data.metadata,
|
||||
content: [...event.data.content],
|
||||
result: event.data.result,
|
||||
}
|
||||
match.executed = event.data.executed || match.executed === true
|
||||
match.providerResultState = event.data.resultState
|
||||
|
|
@ -643,9 +648,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
status: "error",
|
||||
error: event.data.error,
|
||||
input: typeof match.state.input === "string" ? {} : match.state.input,
|
||||
structured: event.data.metadata ?? (match.state.status === "running" ? match.state.structured : {}),
|
||||
content: event.data.content ?? (match.state.status === "running" ? match.state.content : []),
|
||||
result: event.data.result,
|
||||
metadata: event.data.metadata,
|
||||
content: event.data.content,
|
||||
}
|
||||
match.executed = event.data.executed || match.executed === true
|
||||
match.providerResultState = event.data.resultState
|
||||
|
|
|
|||
|
|
@ -342,13 +342,13 @@ function make(state: State, tool: string, input: Record<string, JsonValue>): Ref
|
|||
}
|
||||
}
|
||||
|
||||
function startTool(state: State, ref: Ref, structured: Record<string, JsonValue> = {}): SessionMessageAssistantTool {
|
||||
function startTool(state: State, ref: Ref, metadata: Record<string, JsonValue> = {}): SessionMessageAssistantTool {
|
||||
state.started.add(ref.call)
|
||||
const part = {
|
||||
type: "tool" as const,
|
||||
id: ref.call,
|
||||
name: ref.tool,
|
||||
state: { status: "running" as const, input: ref.input, structured, content: [] },
|
||||
state: { status: "running" as const, input: ref.input, metadata },
|
||||
time: { created: ref.start, ran: ref.start },
|
||||
}
|
||||
present(state, [toolCommit(part, ref.msg, "start")])
|
||||
|
|
@ -395,8 +395,8 @@ function doneTool(
|
|||
state: {
|
||||
status: "completed",
|
||||
input: ref.input,
|
||||
content: output.output ? [{ type: "text", text: output.output }] : [],
|
||||
structured: output.metadata ?? {},
|
||||
content: [{ type: "text", text: output.output }],
|
||||
metadata: output.metadata,
|
||||
},
|
||||
time: { created: ref.start, ran: ref.start, completed: Date.now() },
|
||||
}
|
||||
|
|
@ -415,8 +415,6 @@ function failTool(state: State, ref: Ref, error: string): void {
|
|||
status: "error",
|
||||
input: ref.input,
|
||||
error: { type: "unknown", message: error },
|
||||
structured: {},
|
||||
content: [],
|
||||
},
|
||||
time: { created: ref.start, ran: ref.start, completed: Date.now() },
|
||||
},
|
||||
|
|
@ -527,8 +525,7 @@ function emitTask(state: State): void {
|
|||
offset: 1,
|
||||
limit: 200,
|
||||
},
|
||||
structured: {},
|
||||
content: [],
|
||||
metadata: {},
|
||||
},
|
||||
time: { created: Date.now(), ran: Date.now() },
|
||||
} satisfies SessionMessageAssistantTool
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export function permissionInfo(request: MiniPermissionRequest, directory?: strin
|
|||
resources: request.resources,
|
||||
metadata: request.metadata,
|
||||
input: state?.status === "streaming" ? undefined : state?.input,
|
||||
structured: state?.status === "streaming" ? undefined : state?.structured,
|
||||
toolMetadata: state?.status === "streaming" ? undefined : state?.metadata,
|
||||
},
|
||||
(value) => toolPath(value, { home: true, directory }),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Current-native subagent (child Session) tracking for the mini transport.
|
||||
//
|
||||
// Discovers child Sessions of the active parent from four current sources:
|
||||
// 1. projected subagent tool output (`structured.sessionID`) during hydration
|
||||
// 1. projected subagent tool output (`metadata.sessionID`) during hydration
|
||||
// 2. the current session list filtered by `parentID` during hydration
|
||||
// 3. the process-local active-session map during hydration
|
||||
// 4. live events from unknown sessions whose `parentID` matches the parent
|
||||
|
|
@ -33,6 +33,7 @@ import type {
|
|||
StreamCommit,
|
||||
} from "./types"
|
||||
import { canonicalToolName, normalizeTool, toolOutputText, toolView } from "./tool"
|
||||
import { toolDisplayContent } from "../util/tool-display"
|
||||
|
||||
const CHILD_MESSAGE_LIMIT = 80
|
||||
const CHILD_FRAME_LIMIT = 80
|
||||
|
|
@ -55,7 +56,7 @@ export function toolCommit(
|
|||
): StreamCommit {
|
||||
const part = normalizeTool(input)
|
||||
const status = part.state.status
|
||||
const output = status === "streaming" ? "" : toolOutputText(part.name, part.state.content)
|
||||
const output = status === "streaming" ? "" : toolOutputText(part.name, toolDisplayContent(part.state))
|
||||
const partial = status === "error" && phase === "progress" && value !== undefined
|
||||
const text =
|
||||
status === "running" || partial
|
||||
|
|
@ -178,10 +179,10 @@ function blockerCategory(event: V2Event): "permission" | "form" | undefined {
|
|||
if (event.type === "form.created" || event.type === "form.replied" || event.type === "form.cancelled") return "form"
|
||||
}
|
||||
|
||||
function childSessionID(structured: Record<string, unknown> | undefined) {
|
||||
const sessionID = text(structured?.sessionID)
|
||||
function childSessionID(metadata: Record<string, unknown> | undefined) {
|
||||
const sessionID = text(metadata?.sessionID)
|
||||
if (!sessionID || !sessionID.startsWith("ses")) return undefined
|
||||
const status = structured?.status
|
||||
const status = metadata?.status
|
||||
if (status !== "running" && status !== "completed") return undefined
|
||||
return { sessionID, running: status === "running" }
|
||||
}
|
||||
|
|
@ -199,7 +200,7 @@ function tab(child: ChildState): FooterSubagentTab {
|
|||
|
||||
export function createSubagentTracker(input: SubagentTrackerInput): SubagentTracker {
|
||||
const children = new Map<string, ChildState>()
|
||||
// Live subagent tool calls in the parent, so tool.success structured output
|
||||
// Live subagent tool calls in the parent, so tool.success metadata
|
||||
// can be joined with the call's input metadata.
|
||||
const pendingCalls = new Map<string, Record<string, unknown>>()
|
||||
// Recently resolved non-family sessions. Retention is bounded so unrelated
|
||||
|
|
@ -309,7 +310,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
return
|
||||
}
|
||||
const current = child.tools.get(key)
|
||||
const output = toolOutputText(part.name, part.state.content)
|
||||
const output = toolOutputText(part.name, toolDisplayContent(part.state))
|
||||
if (part.state.status === "running") {
|
||||
if (!current || current.part.state.status === "streaming")
|
||||
setFrame(child, frame, toolCommit(part, messageID, "start", undefined, input.directory))
|
||||
|
|
@ -779,7 +780,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
name: current?.part.name ?? "tool",
|
||||
executed: event.data.executed,
|
||||
providerState: event.data.state,
|
||||
state: { status: "running", input: event.data.input, structured: {}, content: [] },
|
||||
state: { status: "running", input: event.data.input, metadata: {} },
|
||||
time: { created: current?.part.time.created ?? event.created, ran: event.created },
|
||||
},
|
||||
event.data.assistantMessageID,
|
||||
|
|
@ -804,8 +805,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
state: {
|
||||
status: "running",
|
||||
input: part && part.state.status !== "streaming" ? part.state.input : {},
|
||||
structured: event.data.structured,
|
||||
content: event.data.content,
|
||||
metadata: event.data.metadata,
|
||||
},
|
||||
time: {
|
||||
created: part?.time.created ?? event.created,
|
||||
|
|
@ -837,18 +837,15 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
? {
|
||||
status: "error",
|
||||
input: part && part.state.status !== "streaming" ? part.state.input : {},
|
||||
structured:
|
||||
event.data.metadata ?? (part && part.state.status !== "streaming" ? part.state.structured : {}),
|
||||
content: event.data.content ?? (part && part.state.status !== "streaming" ? part.state.content : []),
|
||||
metadata: event.data.metadata,
|
||||
content: event.data.content,
|
||||
error: event.data.error,
|
||||
result: event.data.result,
|
||||
}
|
||||
: {
|
||||
status: "completed",
|
||||
input: part && part.state.status !== "streaming" ? part.state.input : {},
|
||||
structured: event.data.structured,
|
||||
metadata: event.data.metadata,
|
||||
content: event.data.content,
|
||||
result: event.data.result,
|
||||
},
|
||||
time: {
|
||||
created: part?.time.created ?? event.created,
|
||||
|
|
@ -921,7 +918,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
const mainTool = (item: SessionMessageAssistantTool, active?: Record<string, unknown>) => {
|
||||
const tool = normalizeTool(item)
|
||||
if (tool.name !== "subagent" || tool.state.status === "streaming") return
|
||||
const found = childSessionID(record(tool.state.structured))
|
||||
const found = childSessionID(record(tool.state.metadata))
|
||||
if (!found) return
|
||||
const child = admitChild(found.sessionID)
|
||||
if (!child) return
|
||||
|
|
@ -967,7 +964,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
const key = sourceKey(event.data.assistantMessageID, event.data.callID)
|
||||
const pending = pendingCalls.get(key)
|
||||
if (event.type !== "session.tool.progress") pendingCalls.delete(key)
|
||||
const found = childSessionID(record(event.type === "session.tool.failed" ? event.data.metadata : event.data.structured))
|
||||
const found = childSessionID(record(event.data.metadata))
|
||||
if (!found) return
|
||||
const child = admitChild(found.sessionID)
|
||||
if (!child) return
|
||||
|
|
@ -1085,11 +1082,13 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
input.emit()
|
||||
},
|
||||
snapshot() {
|
||||
const tabs = [...children.values()].toSorted((a, b) => {
|
||||
const active = Number(b.status === "running") - Number(a.status === "running")
|
||||
if (active !== 0) return active
|
||||
return b.lastUpdatedAt - a.lastUpdatedAt
|
||||
}).map(tab)
|
||||
const tabs = [...children.values()]
|
||||
.toSorted((a, b) => {
|
||||
const active = Number(b.status === "running") - Number(a.status === "running")
|
||||
if (active !== 0) return active
|
||||
return b.lastUpdatedAt - a.lastUpdatedAt
|
||||
})
|
||||
.map(tab)
|
||||
const child = selected ? children.get(selected) : undefined
|
||||
const details: Record<string, FooterSubagentDetail> =
|
||||
child && !child.detailStale ? { [child.sessionID]: { commits: child.frames.map((item) => item.commit) } } : {}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { writeSessionOutput } from "./stream"
|
|||
import { createFragmentReconciler, fragmentRef, type FragmentReconciler } from "./stream-v2.fragment"
|
||||
import { createSubagentTracker, toolCommit, toolFinalPhase } from "./stream-v2.subagent"
|
||||
import { normalizeTool, toolOutputText } from "./tool"
|
||||
import { toolDisplayContent } from "../util/tool-display"
|
||||
import type {
|
||||
FooterApi,
|
||||
FooterView,
|
||||
|
|
@ -558,7 +559,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
return
|
||||
}
|
||||
const current = state.tools.get(key)
|
||||
const output = toolOutputText(part.name, part.state.content)
|
||||
const output = toolOutputText(part.name, toolDisplayContent(part.state))
|
||||
const prefix = current ? output.startsWith(current.output) : false
|
||||
const version = current && !prefix ? current.version + 1 : (current?.version ?? 0)
|
||||
const delta = current && prefix ? output.slice(current.output.length) : output
|
||||
|
|
@ -670,8 +671,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
{
|
||||
kind: "reasoning",
|
||||
source: "reasoning",
|
||||
text:
|
||||
update.previous.length === 0 ? `Thinking: ${item.text}` : item.text.slice(update.previous.length),
|
||||
text: update.previous.length === 0 ? `Thinking: ${item.text}` : item.text.slice(update.previous.length),
|
||||
phase: "progress",
|
||||
messageID: message.id,
|
||||
partID: fragment.partID,
|
||||
|
|
@ -1042,7 +1042,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
name: current?.part.name ?? "tool",
|
||||
executed: event.data.executed,
|
||||
providerState: event.data.state,
|
||||
state: { status: "running", input: event.data.input, structured: {}, content: [] },
|
||||
state: { status: "running", input: event.data.input, metadata: {} },
|
||||
time: { created: current?.part.time.created ?? event.created, ran: event.created },
|
||||
}
|
||||
renderTool(event.data.assistantMessageID, item)
|
||||
|
|
@ -1062,8 +1062,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
state: {
|
||||
status: "running",
|
||||
input: part && part.state.status !== "streaming" ? part.state.input : {},
|
||||
structured: event.data.structured,
|
||||
content: event.data.content,
|
||||
metadata: event.data.metadata,
|
||||
},
|
||||
time: { created: part?.time.created ?? event.created, ran: part?.time.ran ?? event.created },
|
||||
})
|
||||
|
|
@ -1084,18 +1083,15 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
? {
|
||||
status: "error",
|
||||
input: part && part.state.status !== "streaming" ? part.state.input : {},
|
||||
structured:
|
||||
event.data.metadata ?? (part && part.state.status !== "streaming" ? part.state.structured : {}),
|
||||
content: event.data.content ?? (part && part.state.status !== "streaming" ? part.state.content : []),
|
||||
metadata: event.data.metadata,
|
||||
content: event.data.content,
|
||||
error: event.data.error,
|
||||
result: event.data.result,
|
||||
}
|
||||
: {
|
||||
status: "completed",
|
||||
input: part && part.state.status !== "streaming" ? part.state.input : {},
|
||||
structured: event.data.structured,
|
||||
metadata: event.data.metadata,
|
||||
content: event.data.content,
|
||||
result: event.data.result,
|
||||
},
|
||||
time: { created: part?.time.created ?? event.created, ran: part?.time.ran, completed: event.created },
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ function traceCommit(commit: StreamCommit) {
|
|||
state: {
|
||||
status: commit.part.state.status,
|
||||
input: summarize(commit.part.state.input),
|
||||
structured: "structured" in commit.part.state ? summarize(commit.part.state.structured) : undefined,
|
||||
metadata: "metadata" in commit.part.state ? summarize(commit.part.state.metadata) : undefined,
|
||||
content: "content" in commit.part.state ? summarize(commit.part.state.content) : undefined,
|
||||
error: "error" in commit.part.state ? summarize(commit.part.state.error) : undefined,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export { toolInlineInfo, toolOutputText } from "./tool"
|
||||
export { readDisplayText, toolInlineInfo, toolOutputText } from "./tool"
|
||||
export { nonEmptyToolContent } from "../util/tool-display"
|
||||
export type { MiniToolPart } from "./types"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
canonicalToolName,
|
||||
finiteNumber,
|
||||
primitiveInputSummary,
|
||||
toolDisplayContent,
|
||||
toolDisplayMetadata,
|
||||
webSearchProviderLabel,
|
||||
} from "../util/tool-display"
|
||||
|
|
@ -157,10 +158,36 @@ function text(v: unknown): string {
|
|||
return typeof v === "string" ? v : ""
|
||||
}
|
||||
|
||||
export function toolOutputText(name: string, content: ReadonlyArray<{ type: string; text?: string }>) {
|
||||
export function toolOutputText(name: string, content: ReadonlyArray<{ type: string; text?: string }> | undefined) {
|
||||
if (!content) return ""
|
||||
// V2 shell content appends model-only status after the user-visible command output.
|
||||
if (canonicalToolName(name) === "shell") return content.find((item) => item.type === "text")?.text ?? ""
|
||||
return content.flatMap((item) => (item.type === "text" && item.text ? [item.text] : [])).join("\n")
|
||||
const joined = content.flatMap((item) => (item.type === "text" && item.text ? [item.text] : [])).join("\n")
|
||||
if (canonicalToolName(name) === "read") return readDisplayText(joined) ?? joined
|
||||
return joined
|
||||
}
|
||||
|
||||
/** Read's model content is a JSON page envelope; unwrap the human-facing text. */
|
||||
export function readDisplayText(text: string): string | undefined {
|
||||
if (!text.startsWith("{")) return undefined
|
||||
const parsed = (() => {
|
||||
try {
|
||||
return JSON.parse(text) as unknown
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
})()
|
||||
const envelope = dict(parsed)
|
||||
if (typeof envelope.content === "string" && (envelope.type === "text-page" || envelope.encoding === "utf8"))
|
||||
return envelope.content
|
||||
if (!Array.isArray(envelope.entries)) return undefined
|
||||
return envelope.entries
|
||||
.flatMap((entry): string[] => {
|
||||
if (typeof entry === "string") return [entry]
|
||||
const path = dict(entry).path
|
||||
return typeof path === "string" ? [path] : []
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
function normalizeInput(name: string, value: unknown) {
|
||||
|
|
@ -202,16 +229,16 @@ function normalizeFile(value: unknown): PatchFile | undefined {
|
|||
}
|
||||
}
|
||||
|
||||
function normalizeStructured(name: string, value: unknown) {
|
||||
const structured = dict(value)
|
||||
const files = list(structured.files).flatMap((item) => {
|
||||
function normalizeMetadata(name: string, value: unknown) {
|
||||
const metadata = dict(value)
|
||||
const files = list(metadata.files).flatMap((item) => {
|
||||
const file = normalizeFile(item)
|
||||
return file ? [file] : []
|
||||
})
|
||||
const sessionID = text(structured.sessionID) || text(structured.sessionId)
|
||||
const sessionID = text(metadata.sessionID) || text(metadata.sessionId)
|
||||
return {
|
||||
...structured,
|
||||
...(["edit", "patch"].includes(name) && Array.isArray(structured.files) ? { files } : {}),
|
||||
...metadata,
|
||||
...(["edit", "patch"].includes(name) && Array.isArray(metadata.files) ? { files } : {}),
|
||||
...(name === "subagent" && sessionID ? { sessionID } : {}),
|
||||
}
|
||||
}
|
||||
|
|
@ -225,7 +252,7 @@ export function normalizeTool(tool: SessionMessageAssistantTool): SessionMessage
|
|||
state: {
|
||||
...tool.state,
|
||||
input: normalizeInput(name, tool.state.input),
|
||||
structured: normalizeStructured(name, toolDisplayMetadata(tool.state)),
|
||||
metadata: normalizeMetadata(name, toolDisplayMetadata(tool.state)),
|
||||
},
|
||||
} as SessionMessageAssistantTool
|
||||
}
|
||||
|
|
@ -1089,13 +1116,13 @@ function frame(part: SessionMessageAssistantTool, directory?: string): ToolFrame
|
|||
output: "",
|
||||
time: { start: tool.time.created },
|
||||
}
|
||||
const output = toolOutputText(tool.name, tool.state.content)
|
||||
const output = toolOutputText(tool.name, toolDisplayContent(tool.state))
|
||||
return {
|
||||
directory,
|
||||
raw: output,
|
||||
name: tool.name,
|
||||
input: normalizeInput(tool.name, tool.state.input),
|
||||
meta: normalizeStructured(tool.name, tool.state.structured),
|
||||
meta: normalizeMetadata(tool.name, tool.state.metadata),
|
||||
state: dict(tool.state),
|
||||
status: tool.state.status,
|
||||
error: tool.state.status === "error" ? tool.state.error.message : "",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import {
|
|||
canonicalToolName,
|
||||
finiteNumber,
|
||||
primitiveInputSummary,
|
||||
toolDisplayContent,
|
||||
toolDisplayMetadata,
|
||||
webSearchProviderLabel,
|
||||
} from "../../util/tool-display"
|
||||
|
|
@ -2147,7 +2148,7 @@ function ToolPart(props: { part: SessionMessageAssistantTool }) {
|
|||
},
|
||||
get output() {
|
||||
if (props.part.state.status === "streaming") return undefined
|
||||
return props.part.state.content
|
||||
return toolDisplayContent(props.part.state)
|
||||
.flatMap((content) => (content.type === "text" ? [content.text] : [content.name ?? content.uri]))
|
||||
.join("\n")
|
||||
},
|
||||
|
|
@ -2549,6 +2550,8 @@ function BlockToolContent(props: BlockToolProps & { borderColor: RGBA }) {
|
|||
)
|
||||
}
|
||||
|
||||
const SHELL_DISPLAY_LIMIT = 1024 * 1024
|
||||
|
||||
function Shell(props: ToolProps) {
|
||||
const { themeV2 } = useTheme()
|
||||
const ctx = use()
|
||||
|
|
@ -2560,6 +2563,7 @@ function Shell(props: ToolProps) {
|
|||
})
|
||||
const color = createMemo(() => (permission() ? themeV2.text.feedback.warning.default : themeV2.text.default))
|
||||
const shellID = createMemo(() => stringValue(props.metadata.shellID))
|
||||
const background = createMemo(() => Boolean(shellID()) && props.part.state.status !== "running")
|
||||
const backgroundRunning = createMemo(() => {
|
||||
const id = shellID()
|
||||
return Boolean(id && data.shell.get(id))
|
||||
|
|
@ -2568,31 +2572,73 @@ function Shell(props: ToolProps) {
|
|||
const command = createMemo(() => stringValue(props.input.command))
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
const [backgroundOutput, setBackgroundOutput] = createSignal("")
|
||||
const [outputTruncated, setOutputTruncated] = createSignal(false)
|
||||
let loading = false
|
||||
const loadBackgroundOutput = async () => {
|
||||
let drainRequested = false
|
||||
let cursor = 0
|
||||
let wasRunning = false
|
||||
const loadBackgroundOutput = async (drain = false) => {
|
||||
const id = shellID()
|
||||
if (!id || loading) return
|
||||
if (!id) return
|
||||
if (loading) {
|
||||
if (drain) drainRequested = true
|
||||
return
|
||||
}
|
||||
loading = true
|
||||
const location = data.session.get(ctx.sessionID)?.location
|
||||
await client.api.shell
|
||||
.output({
|
||||
id,
|
||||
limit: 1024 * 1024,
|
||||
location: location ? { directory: location.directory, workspace: location.workspaceID } : undefined,
|
||||
})
|
||||
.then((response) => setBackgroundOutput(stripAnsi(response.data.output.trim())))
|
||||
.catch(() => undefined)
|
||||
do {
|
||||
const response = await client.api.shell
|
||||
.output({
|
||||
id,
|
||||
cursor,
|
||||
limit: SHELL_DISPLAY_LIMIT,
|
||||
location: location ? { directory: location.directory, workspace: location.workspaceID } : undefined,
|
||||
})
|
||||
.catch(() => undefined)
|
||||
if (!response) break
|
||||
if (response.data.output)
|
||||
setBackgroundOutput((output) => {
|
||||
const next = stripAnsi(output + response.data.output)
|
||||
if (next.length <= SHELL_DISPLAY_LIMIT) return next
|
||||
setOutputTruncated(true)
|
||||
return next.slice(-SHELL_DISPLAY_LIMIT)
|
||||
})
|
||||
if (response.data.cursor <= cursor) break
|
||||
cursor = response.data.cursor
|
||||
if (!drain || cursor >= response.data.size) break
|
||||
const tail = Math.max(cursor, response.data.size - SHELL_DISPLAY_LIMIT)
|
||||
if (tail > cursor) {
|
||||
cursor = tail
|
||||
setOutputTruncated(true)
|
||||
}
|
||||
} while (true)
|
||||
loading = false
|
||||
if (drainRequested) {
|
||||
drainRequested = false
|
||||
void loadBackgroundOutput(true)
|
||||
}
|
||||
}
|
||||
createEffect(() => {
|
||||
if (!expanded() || !backgroundRunning()) return
|
||||
const running = backgroundRunning()
|
||||
if (!running) {
|
||||
if (wasRunning) void loadBackgroundOutput(true)
|
||||
wasRunning = false
|
||||
return
|
||||
}
|
||||
wasRunning = true
|
||||
if (background() && !expanded()) return
|
||||
void loadBackgroundOutput()
|
||||
const interval = setInterval(() => void loadBackgroundOutput(), 1_000)
|
||||
onCleanup(() => clearInterval(interval))
|
||||
})
|
||||
const output = createMemo(() => {
|
||||
if (props.part.state.status === "streaming") return ""
|
||||
if (shellID()) return expanded() ? backgroundOutput() : ""
|
||||
const content = props.part.state.content[0]
|
||||
if (shellID()) {
|
||||
if (background() && !expanded()) return ""
|
||||
const text = backgroundOutput().trim()
|
||||
return outputTruncated() ? `[earlier output omitted]\n${text}` : text
|
||||
}
|
||||
const content = toolDisplayContent(props.part.state)[0]
|
||||
return stripAnsi(content?.type === "text" ? content.text.trim() : "")
|
||||
})
|
||||
const maxLines = 10
|
||||
|
|
@ -2608,7 +2654,7 @@ function Shell(props: ToolProps) {
|
|||
const toggle = () => {
|
||||
const next = !expanded()
|
||||
setExpanded(next)
|
||||
if (next) void loadBackgroundOutput()
|
||||
if (next) void loadBackgroundOutput(!backgroundRunning())
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -2639,7 +2685,7 @@ function Shell(props: ToolProps) {
|
|||
</Spinner>
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={shellID()}>
|
||||
<Show when={background()}>
|
||||
<StatusBadge>Background</StatusBadge>
|
||||
</Show>
|
||||
</box>
|
||||
|
|
@ -3183,7 +3229,7 @@ function formatSessionTranscript(session: SessionInfo, messages: SessionMessageI
|
|||
? item.state.error.message
|
||||
: item.state.status === "streaming"
|
||||
? ""
|
||||
: item.state.content
|
||||
: toolDisplayContent(item.state)
|
||||
.flatMap((entry) => (entry.type === "text" ? [entry.text] : [entry.name ?? entry.uri]))
|
||||
.join("\n")
|
||||
return [`**Tool: ${item.name}**\n\n**Input:**\n\`\`\`json\n${input}\n\`\`\`\n\n${output}`]
|
||||
|
|
|
|||
|
|
@ -118,14 +118,14 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
|||
|
||||
const source = createMemo(() => {
|
||||
const tool = props.request.source
|
||||
if (!tool) return { input: undefined, structured: undefined }
|
||||
if (!tool) return { input: undefined, metadata: undefined }
|
||||
const message = data.session.message.get(props.request.sessionID, tool.messageID)
|
||||
if (message?.type !== "assistant") return { input: undefined, structured: undefined }
|
||||
if (message?.type !== "assistant") return { input: undefined, metadata: undefined }
|
||||
const part = message.content.find((part) => part.type === "tool" && part.id === tool.callID)
|
||||
if (part?.type === "tool" && part.state.status !== "streaming") {
|
||||
return { input: part.state.input, structured: part.state.structured }
|
||||
return { input: part.state.input, metadata: part.state.metadata }
|
||||
}
|
||||
return { input: undefined, structured: undefined }
|
||||
return { input: undefined, metadata: undefined }
|
||||
})
|
||||
|
||||
const { themeV2 } = useTheme()
|
||||
|
|
@ -182,7 +182,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
|||
resources: props.request.resources,
|
||||
metadata: props.request.metadata,
|
||||
input: source().input,
|
||||
structured: source().structured,
|
||||
toolMetadata: source().metadata,
|
||||
},
|
||||
pathFormatter.format,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export type PermissionPresentationInput = {
|
|||
resources: ReadonlyArray<unknown>
|
||||
metadata?: unknown
|
||||
input?: unknown
|
||||
structured?: unknown
|
||||
toolMetadata?: unknown
|
||||
}
|
||||
|
||||
export function permissionPresentation(
|
||||
|
|
@ -26,7 +26,7 @@ export function permissionPresentation(
|
|||
): PermissionPresentation {
|
||||
const action = canonicalToolName(source.action)
|
||||
const input = normalizeInput(action, source.input)
|
||||
const metadata = { ...dict(source.structured), ...dict(source.metadata) }
|
||||
const metadata = { ...dict(source.toolMetadata), ...dict(source.metadata) }
|
||||
const resources = source.resources.filter((item): item is string => typeof item === "string")
|
||||
|
||||
if (action === "edit") {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,19 @@ export function webSearchProviderLabel(provider: unknown) {
|
|||
export function toolDisplayMetadata(state: unknown): Record<string, unknown> {
|
||||
if (!state || typeof state !== "object" || Array.isArray(state)) return {}
|
||||
if (!("status" in state) || state.status === "streaming") return {}
|
||||
if (!("structured" in state) || !state.structured || typeof state.structured !== "object") return {}
|
||||
if (Array.isArray(state.structured)) return {}
|
||||
return state.structured as Record<string, unknown>
|
||||
if (!("metadata" in state) || !state.metadata || typeof state.metadata !== "object") return {}
|
||||
if (Array.isArray(state.metadata)) return {}
|
||||
return state.metadata as Record<string, unknown>
|
||||
}
|
||||
|
||||
export function toolDisplayContent(state: SessionMessageAssistantTool["state"]) {
|
||||
if (state.status === "streaming" || state.status === "running") return []
|
||||
return state.content ?? []
|
||||
}
|
||||
|
||||
export function nonEmptyToolContent<T>(content: ReadonlyArray<T> | undefined): [T, ...T[]] | undefined {
|
||||
if (!content) return undefined
|
||||
const [first, ...rest] = content
|
||||
return first === undefined ? undefined : [first, ...rest]
|
||||
}
|
||||
import type { SessionMessageAssistantTool } from "@opencode-ai/client/promise"
|
||||
|
|
|
|||
|
|
@ -2342,8 +2342,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
|||
sessionID: "session-1",
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
callID: "call-1",
|
||||
structured: { sessionID: "session-child", status: "running" },
|
||||
content: [],
|
||||
metadata: { sessionID: "session-child", status: "running" },
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -2353,7 +2352,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
|||
assistant?.type === "assistant" &&
|
||||
assistant.content[0]?.type === "tool" &&
|
||||
assistant.content[0].state.status === "running" &&
|
||||
assistant.content[0].state.structured.sessionID === "session-child"
|
||||
assistant.content[0].state.metadata.sessionID === "session-child"
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -2361,7 +2360,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
|||
id: "evt_failed_1",
|
||||
created: 0,
|
||||
type: "session.tool.failed",
|
||||
durable: durable("session-1", 6),
|
||||
durable: durable("session-1", 6, 2),
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
|
|
@ -2392,8 +2391,8 @@ test("settles pending tools when a live failure arrives", async () => {
|
|||
if (tool.state.status !== "error") return
|
||||
expect(tool.state.error).toEqual({ type: "unknown", message: "aborted" })
|
||||
expect(tool.state.input).toEqual({})
|
||||
expect(tool.state.structured).toEqual({ sessionID: "session-child", status: "running" })
|
||||
expect(tool.state.content).toEqual([])
|
||||
expect(tool.state.metadata).toBeUndefined()
|
||||
expect(tool.state.content).toBeUndefined()
|
||||
expect(tool.executed).toBe(false)
|
||||
expect(tool.providerState).toEqual({ call: true })
|
||||
expect(tool.providerResultState).toEqual({ result: true })
|
||||
|
|
|
|||
|
|
@ -133,8 +133,8 @@ describe("run entry body", () => {
|
|||
path: "src/a.ts",
|
||||
content: "const x = 1\n",
|
||||
},
|
||||
structured: {},
|
||||
content: [],
|
||||
metadata: {},
|
||||
content: [{ type: "text", text: "" }],
|
||||
},
|
||||
}),
|
||||
snapshot: {
|
||||
|
|
@ -153,10 +153,10 @@ describe("run entry body", () => {
|
|||
input: {
|
||||
path: "src/a.ts",
|
||||
},
|
||||
structured: {
|
||||
metadata: {
|
||||
files: [{ file: "src/a.ts", status: "modified", patch: "@@ -1 +1 @@\n-old\n+new\n" }],
|
||||
},
|
||||
content: [],
|
||||
content: [{ type: "text", text: "" }],
|
||||
},
|
||||
}),
|
||||
snapshot: {
|
||||
|
|
@ -177,8 +177,8 @@ describe("run entry body", () => {
|
|||
state: {
|
||||
status: "completed",
|
||||
input: {},
|
||||
content: [],
|
||||
structured: {
|
||||
content: [{ type: "text", text: "" }],
|
||||
metadata: {
|
||||
files: [
|
||||
{
|
||||
status: "modified",
|
||||
|
|
@ -221,8 +221,7 @@ describe("run entry body", () => {
|
|||
description: "Inspect reducer",
|
||||
agent: "explore",
|
||||
},
|
||||
structured: { sessionID: "ses-child-1", status: "running" },
|
||||
content: [],
|
||||
metadata: { sessionID: "ses-child-1", status: "running" },
|
||||
},
|
||||
}),
|
||||
),
|
||||
|
|
@ -243,7 +242,7 @@ describe("run entry body", () => {
|
|||
agent: "explore",
|
||||
},
|
||||
content: [{ type: "text", text: "# Findings\n\n- Footer stays live" }],
|
||||
structured: {
|
||||
metadata: {
|
||||
sessionID: "ses-child-1",
|
||||
status: "completed",
|
||||
output: "# Findings\n\n- Footer stays live",
|
||||
|
|
@ -266,8 +265,8 @@ describe("run entry body", () => {
|
|||
description: "Inspect reducer",
|
||||
agent: "explore",
|
||||
},
|
||||
content: [],
|
||||
structured: {
|
||||
content: [{ type: "text", text: "" }],
|
||||
metadata: {
|
||||
sessionID: "ses-child-1",
|
||||
status: "completed",
|
||||
output: "",
|
||||
|
|
@ -341,7 +340,7 @@ describe("run entry body", () => {
|
|||
workdir: "/tmp/demo",
|
||||
},
|
||||
content: [{ type: "text", text: output }],
|
||||
structured: { exit: 0, truncated: false },
|
||||
metadata: { exit: 0, truncated: false },
|
||||
},
|
||||
}),
|
||||
),
|
||||
|
|
@ -364,8 +363,7 @@ describe("run entry body", () => {
|
|||
input: {
|
||||
command: "ls",
|
||||
},
|
||||
structured: {},
|
||||
content: [],
|
||||
metadata: {},
|
||||
},
|
||||
}),
|
||||
),
|
||||
|
|
@ -435,8 +433,8 @@ describe("run entry body", () => {
|
|||
input: {
|
||||
patchText: "*** Begin Patch\n*** End Patch",
|
||||
},
|
||||
content: [],
|
||||
structured: {
|
||||
content: [{ type: "text", text: "" }],
|
||||
metadata: {
|
||||
files: [
|
||||
{
|
||||
status: "modified",
|
||||
|
|
@ -463,8 +461,8 @@ describe("run entry body", () => {
|
|||
input: {
|
||||
patchText: "*** Begin Patch\n*** End Patch",
|
||||
},
|
||||
content: [],
|
||||
structured: {
|
||||
content: [{ type: "text", text: "" }],
|
||||
metadata: {
|
||||
files: [
|
||||
{
|
||||
status: "modified",
|
||||
|
|
@ -499,8 +497,7 @@ describe("run entry body", () => {
|
|||
path: "/tmp/demo/run",
|
||||
},
|
||||
error: { type: "unknown", message: "No such file or directory: '/tmp/demo/run'" },
|
||||
structured: {},
|
||||
content: [],
|
||||
metadata: {},
|
||||
},
|
||||
}),
|
||||
),
|
||||
|
|
@ -520,11 +517,11 @@ describe("run entry body", () => {
|
|||
state: {
|
||||
status: "completed",
|
||||
input: { target: "demo" },
|
||||
structured: {
|
||||
metadata: {
|
||||
result: { ok: true, nested: { values: Array.from({ length: 40 }, (_, index) => ({ index })) } },
|
||||
large: "x".repeat(8_000),
|
||||
},
|
||||
content: [],
|
||||
content: [{ type: "text", text: "" }],
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -95,8 +95,7 @@ describe("run permission shared", () => {
|
|||
{
|
||||
status: "running",
|
||||
input: { command: "git status --short" },
|
||||
structured: {},
|
||||
content: [],
|
||||
metadata: {},
|
||||
},
|
||||
"call-shell",
|
||||
),
|
||||
|
|
@ -141,8 +140,7 @@ describe("run permission shared", () => {
|
|||
{
|
||||
status: "running",
|
||||
input: { query: "current releases" },
|
||||
structured: { provider: "exa", retained: true },
|
||||
content: [],
|
||||
metadata: { provider: "exa", retained: true },
|
||||
},
|
||||
"call-search",
|
||||
),
|
||||
|
|
@ -165,8 +163,7 @@ describe("run permission shared", () => {
|
|||
{
|
||||
status: "running",
|
||||
input: { patchText: patch },
|
||||
structured: {},
|
||||
content: [],
|
||||
metadata: {},
|
||||
},
|
||||
"call-edit",
|
||||
),
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ test("renders monochrome scrollback as ASCII markdown", async () => {
|
|||
try {
|
||||
await out.scrollback.append(assistant("# H"))
|
||||
expect(Reflect.get(out.scrollback, "active")?.renderable).toBeInstanceOf(MarkdownRenderable)
|
||||
await out.scrollback.append(assistant('éading →\n\n> “quote”\n\n---\n\n| A | B |\n| - | - |\n| α | β |'))
|
||||
await out.scrollback.append(assistant("éading →\n\n> “quote”\n\n---\n\n| A | B |\n| - | - |\n| α | β |"))
|
||||
await out.scrollback.complete()
|
||||
out.renderer.writeToScrollback((ctx) => ({
|
||||
root: new TextRenderable(ctx.renderContext, {
|
||||
|
|
@ -386,8 +386,7 @@ test("renders question summaries without boilerplate footer copy", async () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
structured: {},
|
||||
content: [],
|
||||
metadata: {},
|
||||
},
|
||||
}),
|
||||
final: toolCommit({
|
||||
|
|
@ -406,10 +405,10 @@ test("renders question summaries without boilerplate footer copy", async () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
structured: {
|
||||
metadata: {
|
||||
answers: [["Bug fix"]],
|
||||
},
|
||||
content: [],
|
||||
content: [{ type: "text", text: "" }],
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
|
@ -481,8 +480,7 @@ test("inserts spacers for new visible groups", async () => {
|
|||
input: {
|
||||
pattern: "**/run.ts",
|
||||
},
|
||||
structured: {},
|
||||
content: [],
|
||||
metadata: {},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
|
@ -617,8 +615,7 @@ test("does not double-space before completed shell output when inline tool heade
|
|||
command: "ls",
|
||||
workdir: "src/cli/cmd/run",
|
||||
},
|
||||
structured: {},
|
||||
content: [],
|
||||
metadata: {},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
|
@ -634,8 +631,7 @@ test("does not double-space before completed shell output when inline tool heade
|
|||
pattern: "**/*tool*",
|
||||
path: "src/cli/cmd/run",
|
||||
},
|
||||
structured: {},
|
||||
content: [],
|
||||
metadata: {},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
|
@ -651,8 +647,7 @@ test("does not double-space before completed shell output when inline tool heade
|
|||
pattern: "tool",
|
||||
path: "src/cli/cmd/run",
|
||||
},
|
||||
structured: {},
|
||||
content: [],
|
||||
metadata: {},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
|
@ -670,7 +665,7 @@ test("does not double-space before completed shell output when inline tool heade
|
|||
workdir: "src/cli/cmd/run",
|
||||
},
|
||||
content: [{ type: "text", text: ["src/cli/cmd/run", "ls", "demo.ts", "entry.body.ts", "", ""].join("\n") }],
|
||||
structured: { exit: 0, truncated: false },
|
||||
metadata: { exit: 0, truncated: false },
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
|
@ -735,8 +730,7 @@ test("renders structured write finals once as code blocks", async () => {
|
|||
path: "src/a.ts",
|
||||
content: "const x = 1\nconst y = 2\n",
|
||||
},
|
||||
structured: {},
|
||||
content: [],
|
||||
metadata: {},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
|
@ -755,8 +749,8 @@ test("renders structured write finals once as code blocks", async () => {
|
|||
path: "src/a.ts",
|
||||
content: "const x = 1\nconst y = 2\n",
|
||||
},
|
||||
structured: {},
|
||||
content: [],
|
||||
metadata: {},
|
||||
content: [{ type: "text", text: "" }],
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -234,8 +234,7 @@ describe("V2 mini transport", () => {
|
|||
sessionID: "ses_1",
|
||||
thinking: false,
|
||||
footer: ui.api,
|
||||
contextLimit: (model) =>
|
||||
model.providerID === "test" && model.modelID === "model" ? 160_000 : undefined,
|
||||
contextLimit: (model) => (model.providerID === "test" && model.modelID === "model" ? 160_000 : undefined),
|
||||
})
|
||||
|
||||
events.push({
|
||||
|
|
@ -324,8 +323,7 @@ describe("V2 mini transport", () => {
|
|||
{
|
||||
status: "running" as const,
|
||||
input: { command: "git status --short" },
|
||||
structured: {},
|
||||
content: [],
|
||||
metadata: {},
|
||||
},
|
||||
"call_child_source",
|
||||
),
|
||||
|
|
@ -620,8 +618,8 @@ describe("V2 mini transport", () => {
|
|||
expect.objectContaining({ kind: "user", messageID: "msg_queued", text: "follow up" }),
|
||||
)
|
||||
expect(pending()).toEqual([])
|
||||
const prompt = spyOn(client.session, "prompt").mockImplementation((request) =>
|
||||
ok({ ...promptAdmission(request), admittedSeq: 2 }) as never,
|
||||
const prompt = spyOn(client.session, "prompt").mockImplementation(
|
||||
(request) => ok({ ...promptAdmission(request), admittedSeq: 2 }) as never,
|
||||
)
|
||||
await transport.queuePromptTurn({
|
||||
agent: "review",
|
||||
|
|
@ -631,10 +629,7 @@ describe("V2 mini transport", () => {
|
|||
files: [],
|
||||
includeFiles: false,
|
||||
})
|
||||
expect(client.session.switchAgent).toHaveBeenCalledWith(
|
||||
{ sessionID: "ses_1", agent: "review" },
|
||||
expect.anything(),
|
||||
)
|
||||
expect(client.session.switchAgent).toHaveBeenCalledWith({ sessionID: "ses_1", agent: "review" }, expect.anything())
|
||||
expect(prompt).toHaveBeenCalledWith(expect.objectContaining({ delivery: "queue" }), expect.anything())
|
||||
events.push({
|
||||
id: "evt_earlier_admission",
|
||||
|
|
@ -1976,13 +1971,13 @@ describe("V2 mini transport", () => {
|
|||
id: `evt_repeated_success_${index}`,
|
||||
created: index * 3 + 3,
|
||||
type: "session.tool.success",
|
||||
durable: durable("ses_1", index * 3 + 2),
|
||||
durable: durable("ses_1", index * 3 + 2, 2),
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: messageID,
|
||||
callID: "call_repeated",
|
||||
structured: {},
|
||||
content: [],
|
||||
metadata: {},
|
||||
content: [{ type: "text", text: "" }],
|
||||
executed: true,
|
||||
},
|
||||
})
|
||||
|
|
@ -2048,15 +2043,14 @@ describe("V2 mini transport", () => {
|
|||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_progress",
|
||||
callID: "call_progress",
|
||||
structured: { checkpoint: 1 },
|
||||
content: [{ type: "text", text: "partial" }],
|
||||
metadata: { checkpoint: 1 },
|
||||
},
|
||||
})
|
||||
events.push({
|
||||
id: "evt_progress_failed",
|
||||
created: 4,
|
||||
type: "session.tool.failed",
|
||||
durable: durable("ses_1", 3),
|
||||
durable: durable("ses_1", 3, 2),
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_progress",
|
||||
|
|
@ -2077,7 +2071,7 @@ describe("V2 mini transport", () => {
|
|||
])
|
||||
expect(commits.at(-1)?.part?.state).toMatchObject({
|
||||
status: "error",
|
||||
structured: { checkpoint: 1 },
|
||||
metadata: { checkpoint: 1 },
|
||||
content: [{ type: "text", text: "partial" }],
|
||||
})
|
||||
await transport.close()
|
||||
|
|
@ -2649,9 +2643,7 @@ describe("V2 mini transport", () => {
|
|||
],
|
||||
command: { name: "deploy", arguments: "prod" },
|
||||
},
|
||||
files: [
|
||||
{ type: "file", url: "file:///tmp/context.txt", filename: "context.txt", mime: "text/plain" },
|
||||
],
|
||||
files: [{ type: "file", url: "file:///tmp/context.txt", filename: "context.txt", mime: "text/plain" }],
|
||||
includeFiles: true,
|
||||
})
|
||||
|
||||
|
|
@ -2732,10 +2724,7 @@ describe("V2 mini transport", () => {
|
|||
includeFiles: true,
|
||||
})
|
||||
|
||||
expect(client.session.switchAgent).toHaveBeenCalledWith(
|
||||
{ sessionID: "ses_1", agent: "review" },
|
||||
expect.anything(),
|
||||
)
|
||||
expect(client.session.switchAgent).toHaveBeenCalledWith({ sessionID: "ses_1", agent: "review" }, expect.anything())
|
||||
expect(request).toMatchObject({ sessionID: "ses_1", id: "msg_skill", skill: "tigerstyle" })
|
||||
expect(command).not.toHaveBeenCalled()
|
||||
expect(prompt).not.toHaveBeenCalled()
|
||||
|
|
@ -2886,7 +2875,7 @@ describe("V2 mini transport", () => {
|
|||
id: "evt_failed_subagent",
|
||||
created: 3,
|
||||
type: "session.tool.failed",
|
||||
durable: durable("ses_1", 2),
|
||||
durable: durable("ses_1", 2, 2),
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_failed_subagent",
|
||||
|
|
@ -2897,8 +2886,7 @@ describe("V2 mini transport", () => {
|
|||
},
|
||||
})
|
||||
|
||||
while (!states().some((state) => state.tabs.some((tab) => tab.sessionID === "ses_child_failed")))
|
||||
await Bun.sleep(0)
|
||||
while (!states().some((state) => state.tabs.some((tab) => tab.sessionID === "ses_child_failed"))) await Bun.sleep(0)
|
||||
expect(states().at(-1)?.tabs).toMatchObject([
|
||||
{
|
||||
sessionID: "ses_child_failed",
|
||||
|
|
@ -2954,8 +2942,7 @@ describe("V2 mini transport", () => {
|
|||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_subagent",
|
||||
callID: "call_subagent",
|
||||
structured: { sessionID: "ses_child_progress", status: "running" },
|
||||
content: [],
|
||||
metadata: { sessionID: "ses_child_progress", status: "running" },
|
||||
},
|
||||
})
|
||||
while (!states().some((state) => state.tabs.some((tab) => tab.sessionID === "ses_child_progress")))
|
||||
|
|
@ -3005,8 +2992,7 @@ describe("V2 mini transport", () => {
|
|||
sessionID: "ses_child_progress",
|
||||
assistantMessageID: "msg_child_tool",
|
||||
callID: "call_child_shell",
|
||||
structured: { checkpoint: "child" },
|
||||
content: [{ type: "text", text: "child partial" }],
|
||||
metadata: { checkpoint: "child" },
|
||||
},
|
||||
})
|
||||
events.push({
|
||||
|
|
@ -3025,7 +3011,7 @@ describe("V2 mini transport", () => {
|
|||
id: "evt_child_tool_failed",
|
||||
created: 8,
|
||||
type: "session.tool.failed",
|
||||
durable: durable("ses_child_progress", 3),
|
||||
durable: durable("ses_child_progress", 3, 2),
|
||||
data: {
|
||||
sessionID: "ses_child_progress",
|
||||
assistantMessageID: "msg_child_tool",
|
||||
|
|
@ -3058,7 +3044,7 @@ describe("V2 mini transport", () => {
|
|||
commits.find((item) => item.part?.id === "call_child_shell" && item.toolState === "error")?.part?.state,
|
||||
).toMatchObject({
|
||||
status: "error",
|
||||
structured: { checkpoint: "child" },
|
||||
metadata: { checkpoint: "child" },
|
||||
content: [{ type: "text", text: "child partial" }],
|
||||
})
|
||||
expect(
|
||||
|
|
@ -3145,7 +3131,11 @@ describe("V2 mini transport", () => {
|
|||
{ sessionID: "ses_child", label: "Explore", title: "Find files", status: "running" },
|
||||
])
|
||||
|
||||
expect(states().at(-1)?.details.ses_child?.commits.filter((item) => item.text === "child answer")).toHaveLength(1)
|
||||
expect(
|
||||
states()
|
||||
.at(-1)
|
||||
?.details.ses_child?.commits.filter((item) => item.text === "child answer"),
|
||||
).toHaveLength(1)
|
||||
|
||||
events.push({
|
||||
id: "evt_child_text_replayed",
|
||||
|
|
@ -3159,7 +3149,11 @@ describe("V2 mini transport", () => {
|
|||
},
|
||||
})
|
||||
await Bun.sleep(0)
|
||||
expect(states().at(-1)?.details.ses_child?.commits.filter((item) => item.text === "child answer")).toHaveLength(1)
|
||||
expect(
|
||||
states()
|
||||
.at(-1)
|
||||
?.details.ses_child?.commits.filter((item) => item.text === "child answer"),
|
||||
).toHaveLength(1)
|
||||
|
||||
events.push({
|
||||
id: "evt_child_text_suffix",
|
||||
|
|
@ -3172,7 +3166,9 @@ describe("V2 mini transport", () => {
|
|||
delta: " suffix",
|
||||
},
|
||||
})
|
||||
while (!states().some((state) => state.details.ses_child?.commits.some((item) => item.text === "child answer suffix")))
|
||||
while (
|
||||
!states().some((state) => state.details.ses_child?.commits.some((item) => item.text === "child answer suffix"))
|
||||
)
|
||||
await Bun.sleep(0)
|
||||
|
||||
events.push({
|
||||
|
|
@ -3437,7 +3433,7 @@ describe("V2 mini transport", () => {
|
|||
status: "completed" as const,
|
||||
input: { command: "projected" },
|
||||
content: [{ type: "text" as const, text: "projected result" }],
|
||||
structured: {},
|
||||
metadata: {},
|
||||
},
|
||||
time: { created: 1, ran: 1, completed: 2 },
|
||||
},
|
||||
|
|
@ -3488,12 +3484,12 @@ describe("V2 mini transport", () => {
|
|||
id: "evt_success_terminal",
|
||||
created: 2,
|
||||
type: "session.tool.success",
|
||||
durable: durable("ses_child", 2),
|
||||
durable: durable("ses_child", 2, 2),
|
||||
data: {
|
||||
sessionID: "ses_child",
|
||||
assistantMessageID: "msg_tool_projected",
|
||||
callID: "call_terminal",
|
||||
structured: {},
|
||||
metadata: {},
|
||||
content: [{ type: "text", text: "found" }],
|
||||
executed: true,
|
||||
},
|
||||
|
|
@ -3651,13 +3647,13 @@ describe("V2 mini transport", () => {
|
|||
id: "evt_parent_success",
|
||||
created: 0,
|
||||
type: "session.tool.success",
|
||||
durable: durable("ses_1", 1),
|
||||
durable: durable("ses_1", 1, 2),
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_parent_a",
|
||||
callID: "call_sub",
|
||||
structured: { sessionID: "ses_child", status: "running", output: "" },
|
||||
content: [],
|
||||
metadata: { sessionID: "ses_child", status: "running", output: "" },
|
||||
content: [{ type: "text", text: "" }],
|
||||
executed: true,
|
||||
},
|
||||
})
|
||||
|
|
@ -3738,7 +3734,7 @@ describe("V2 mini transport", () => {
|
|||
status: "completed" as const,
|
||||
input: { agent: "explore", description: "Find things", prompt: "go" },
|
||||
content: [{ type: "text" as const, text: "done" }],
|
||||
structured: { sessionID: "ses_child", status: "completed", output: "done" },
|
||||
metadata: { sessionID: "ses_child", status: "completed", output: "done" },
|
||||
},
|
||||
time: { created: 1, ran: 1, completed: 2 },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ describe("Mini tool presentation", () => {
|
|||
state: {
|
||||
status: "completed",
|
||||
input: { patchText: "*** Begin Patch\n*** End Patch" },
|
||||
structured: {
|
||||
metadata: {
|
||||
files: [
|
||||
{
|
||||
type: "update",
|
||||
|
|
@ -45,7 +45,7 @@ describe("Mini tool presentation", () => {
|
|||
).toMatchObject({
|
||||
name: "patch",
|
||||
state: {
|
||||
structured: {
|
||||
metadata: {
|
||||
files: [
|
||||
{
|
||||
status: "modified",
|
||||
|
|
@ -66,27 +66,25 @@ describe("Mini tool presentation", () => {
|
|||
state: {
|
||||
status: "running",
|
||||
input: { subagent_type: "explore", description: "Inspect" },
|
||||
structured: {},
|
||||
content: [],
|
||||
metadata: {},
|
||||
},
|
||||
time: { created: 1, ran: 1 },
|
||||
}),
|
||||
).toMatchObject({ name: "subagent", state: { input: { agent: "explore" } } })
|
||||
})
|
||||
|
||||
test("renders the skill name from structured metadata with the input id as fallback", () => {
|
||||
const skill = (structured: { name?: string }) => ({
|
||||
type: "tool" as const,
|
||||
id: "call-skill",
|
||||
name: "skill",
|
||||
state: {
|
||||
status: "completed" as const,
|
||||
input: { id: "tigerstyle" },
|
||||
structured,
|
||||
content: [],
|
||||
},
|
||||
time: { created: 1, ran: 1, completed: 2 },
|
||||
})
|
||||
test("renders the skill name from tool metadata with the input id as fallback", () => {
|
||||
const skill = (metadata: { name?: string }) =>
|
||||
canonicalToolPart(
|
||||
"skill",
|
||||
{
|
||||
status: "completed",
|
||||
input: { id: "tigerstyle" },
|
||||
metadata,
|
||||
content: [{ type: "text", text: "" }],
|
||||
},
|
||||
"call-skill",
|
||||
)
|
||||
|
||||
expect(toolInlineInfo(skill({ name: "effect" })).title).toBe('Skill "effect"')
|
||||
expect(toolInlineInfo(skill({})).title).toBe('Skill "tigerstyle"')
|
||||
|
|
@ -112,8 +110,8 @@ describe("Mini tool presentation", () => {
|
|||
canonicalToolPart("glob", {
|
||||
status: "completed",
|
||||
input: { pattern: "*.ts" },
|
||||
structured: { count: 3 },
|
||||
content: [],
|
||||
metadata: { count: 3 },
|
||||
content: [{ type: "text", text: "" }],
|
||||
}),
|
||||
).description,
|
||||
).toBe("3 matches")
|
||||
|
|
@ -122,8 +120,8 @@ describe("Mini tool presentation", () => {
|
|||
canonicalToolPart("grep", {
|
||||
status: "completed",
|
||||
input: { pattern: "needle" },
|
||||
structured: { matches: 1 },
|
||||
content: [],
|
||||
metadata: { matches: 1 },
|
||||
content: [{ type: "text", text: "" }],
|
||||
}),
|
||||
).description,
|
||||
).toBe("1 match")
|
||||
|
|
|
|||
|
|
@ -40,19 +40,19 @@ describe("webSearchProviderLabel", () => {
|
|||
})
|
||||
|
||||
describe("toolDisplayMetadata", () => {
|
||||
test("returns structured metadata for non-pending states", () => {
|
||||
const structured = { provider: "parallel", numResults: 3 }
|
||||
test("returns tool metadata for non-pending states", () => {
|
||||
const metadata = { provider: "parallel", numResults: 3 }
|
||||
|
||||
expect(toolDisplayMetadata({ status: "running", structured })).toBe(structured)
|
||||
expect(toolDisplayMetadata({ status: "completed", structured })).toBe(structured)
|
||||
expect(toolDisplayMetadata({ status: "error", structured })).toBe(structured)
|
||||
expect(toolDisplayMetadata({ status: "running", metadata })).toBe(metadata)
|
||||
expect(toolDisplayMetadata({ status: "completed", metadata })).toBe(metadata)
|
||||
expect(toolDisplayMetadata({ status: "error", metadata })).toBe(metadata)
|
||||
})
|
||||
|
||||
test("does not expose pending or malformed metadata", () => {
|
||||
expect(toolDisplayMetadata({ status: "streaming", structured: { provider: "exa" } })).toEqual({})
|
||||
expect(toolDisplayMetadata({ status: "streaming", metadata: { provider: "exa" } })).toEqual({})
|
||||
expect(toolDisplayMetadata({ status: "completed" })).toEqual({})
|
||||
expect(toolDisplayMetadata({ status: "completed", structured: null })).toEqual({})
|
||||
expect(toolDisplayMetadata({ status: "completed", structured: [] })).toEqual({})
|
||||
expect(toolDisplayMetadata({ status: "completed", metadata: null })).toEqual({})
|
||||
expect(toolDisplayMetadata({ status: "completed", metadata: [] })).toEqual({})
|
||||
expect(toolDisplayMetadata(undefined)).toEqual({})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue