diff --git a/packages/opencode/src/cli/cmd/run/demo.ts b/packages/opencode/src/cli/cmd/run/demo.ts index 6305c413f6..86fbdc9264 100644 --- a/packages/opencode/src/cli/cmd/run/demo.ts +++ b/packages/opencode/src/cli/cmd/run/demo.ts @@ -14,7 +14,7 @@ // Demo mode also handles permission and question replies locally, completing // or failing the synthetic tool parts as appropriate. import path from "path" -import type { Event } from "@opencode-ai/sdk/v2" +import type { Event, ToolPart } from "@opencode-ai/sdk/v2" import { createSessionData, reduceSessionData, type SessionData } from "./session-data" import { writeSessionOutput } from "./stream" import type { @@ -48,6 +48,16 @@ const QUESTIONS = ["multi", "single", "checklist", "custom"] as const type PermissionKind = (typeof PERMISSIONS)[number] type QuestionKind = (typeof QUESTIONS)[number] +function permissionKind(value: string | undefined): PermissionKind | undefined { + const next = (value || "edit").toLowerCase() + return PERMISSIONS.find((item) => item === next) +} + +function questionKind(value: string | undefined): QuestionKind | undefined { + const next = (value || "multi").toLowerCase() + return QUESTIONS.find((item) => item === next) +} + const SAMPLE_MARKDOWN = [ "# Direct Mode Demo", "", @@ -565,16 +575,19 @@ function failTool(state: State, ref: Ref, error: string): void { } function emitError(state: State, text: string): void { - feed(state, { + const event = { type: "session.error", properties: { sessionID: state.id, error: { - name: "DemoError", - message: text, + name: "UnknownError", + data: { + message: text, + }, }, }, - } as unknown as Event) + } satisfies Event + feed(state, event) } async function emitBash(state: State, signal?: AbortSignal): Promise { @@ -663,6 +676,25 @@ function emitTask(state: State): void { sessionId: "sub_demo_1", }, }) + const part = { + id: "sub_demo_tool_1", + type: "tool", + sessionID: "sub_demo_1", + messageID: "sub_demo_msg_tool", + callID: "sub_demo_call_1", + tool: "read", + state: { + status: "running", + input: { + filePath: "packages/opencode/src/cli/cmd/run/stream.ts", + offset: 1, + limit: 200, + }, + time: { + start: Date.now(), + }, + }, + } satisfies ToolPart showSubagent(state, { sessionID: "sub_demo_1", partID: ref.part, @@ -695,25 +727,7 @@ function emitTask(state: State): void { messageID: "sub_demo_msg_tool", partID: "sub_demo_tool_1", tool: "read", - part: { - id: "sub_demo_tool_1", - type: "tool", - sessionID: "sub_demo_1", - messageID: "sub_demo_msg_tool", - callID: "sub_demo_call_1", - tool: "read", - state: { - status: "running", - input: { - filePath: "packages/opencode/src/cli/cmd/run/stream.ts", - offset: 1, - limit: 200, - }, - time: { - start: Date.now(), - }, - }, - } as never, + part, }, { kind: "assistant", @@ -1160,8 +1174,8 @@ export function createRunDemo(input: Input) { } if (cmd === "/permission") { - const kind = (list[1] || "edit").toLowerCase() as PermissionKind - if (!PERMISSIONS.includes(kind)) { + const kind = permissionKind(list[1]) + if (!kind) { note(state.footer, `Pick a permission kind: ${PERMISSIONS.join(", ")}`) return true } @@ -1171,8 +1185,8 @@ export function createRunDemo(input: Input) { } if (cmd === "/question") { - const kind = (list[1] || "multi").toLowerCase() as QuestionKind - if (!QUESTIONS.includes(kind)) { + const kind = questionKind(list[1]) + if (!kind) { note(state.footer, `Pick a question kind: ${QUESTIONS.join(", ")}`) return true } @@ -1194,7 +1208,7 @@ export function createRunDemo(input: Input) { return true } - note(state.footer, `Unknown kind \"${kind}\". Use: ${KINDS.join(", ")}`) + note(state.footer, `Unknown kind "${kind}". Use: ${KINDS.join(", ")}`) return true } @@ -1203,19 +1217,20 @@ export function createRunDemo(input: Input) { const permission = (input: PermissionReply): boolean => { const item = state.perms.get(input.requestID) - if (!item) { + if (!item || !input.reply) { return false } state.perms.delete(input.requestID) - feed(state, { + const event = { type: "permission.replied", properties: { sessionID: state.id, requestID: input.requestID, reply: input.reply, }, - } as Event) + } satisfies Event + feed(state, event) if (input.reply === "reject") { failTool(state, item.ref, input.message || "permission rejected") @@ -1228,19 +1243,20 @@ export function createRunDemo(input: Input) { const questionReply = (input: QuestionReply): boolean => { const ask = state.asks.get(input.requestID) - if (!ask) { + if (!ask || !input.answers) { return false } state.asks.delete(input.requestID) - feed(state, { + const event = { type: "question.replied", properties: { sessionID: state.id, requestID: input.requestID, answers: input.answers, }, - } as Event) + } satisfies Event + feed(state, event) doneTool(state, ask.ref, { title: "question", output: "", diff --git a/packages/opencode/src/cli/cmd/run/footer.subagent.tsx b/packages/opencode/src/cli/cmd/run/footer.subagent.tsx index d09b3a059d..c026088ee6 100644 --- a/packages/opencode/src/cli/cmd/run/footer.subagent.tsx +++ b/packages/opencode/src/cli/cmd/run/footer.subagent.tsx @@ -173,7 +173,7 @@ export function RunFooterSubagentBody(props: { stickyStart="bottom" verticalScrollbarOptions={scrollbar()} ref={(item) => { - scroll = item as ScrollBoxRenderable + scroll = item }} > diff --git a/packages/opencode/src/cli/cmd/run/footer.ts b/packages/opencode/src/cli/cmd/run/footer.ts index 684541c97e..bb550bc425 100644 --- a/packages/opencode/src/cli/cmd/run/footer.ts +++ b/packages/opencode/src/cli/cmd/run/footer.ts @@ -136,6 +136,8 @@ function eventPatch(next: FooterEvent): FooterPatch | undefined { if (next.type === "stream.patch") { return next.patch } + + return undefined } export class RunFooter implements FooterApi { @@ -187,14 +189,14 @@ export class RunFooter implements FooterApi { const [view, setView] = createSignal({ type: "prompt" }) this.view = view this.setView = setView - const [agents, setAgents] = createSignal(options.agents) + const [agents, setAgents] = createSignal(options.agents) this.agents = agents this.setAgents = setAgents - const [resources, setResources] = createSignal(options.resources) + const [resources, setResources] = createSignal(options.resources) this.resources = resources this.setResources = setResources const [subagent, setSubagent] = createStore(createEmptySubagentState()) - this.subagent = () => subagent as FooterSubagentState + this.subagent = () => subagent this.setSubagent = (next) => { setSubagent("tabs", reconcile(next.tabs, { key: "sessionID" })) setSubagent("details", reconcile(next.details)) @@ -239,7 +241,7 @@ export class RunFooter implements FooterApi { onStatus: this.setStatus, onSubagentSelect: options.onSubagentSelect, }), - this.renderer as unknown as Parameters[1], + this.renderer, ).catch(() => { if (!this.isGone) { this.close() diff --git a/packages/opencode/src/cli/cmd/run/footer.view.tsx b/packages/opencode/src/cli/cmd/run/footer.view.tsx index 60a96cfd14..fcde0b8517 100644 --- a/packages/opencode/src/cli/cmd/run/footer.view.tsx +++ b/packages/opencode/src/cli/cmd/run/footer.view.tsx @@ -84,11 +84,11 @@ function subagentShortcut(event: { super?: boolean }): number | undefined { if (!event.ctrl || event.meta || event.super) { - return + return undefined } if (!/^[0-9]$/.test(event.name)) { - return + return undefined } const slot = Number(event.name) @@ -121,11 +121,7 @@ export function RunFooterView(props: RunFooterViewProps) { const showTabs = createMemo(() => active().type === "prompt" && tabs().length > 0) const detail = createMemo(() => { const current = route() - if (current.type !== "subagent") { - return - } - - return subagent().details[current.sessionID] + return current.type === "subagent" ? subagent().details[current.sessionID] : undefined }) const variant = createMemo(() => printableBinding(props.keybinds.variantCycle, props.keybinds.leader)) const interrupt = createMemo(() => printableBinding(props.keybinds.interrupt, props.keybinds.leader)) diff --git a/packages/opencode/src/cli/cmd/run/otel.ts b/packages/opencode/src/cli/cmd/run/otel.ts index d4f20c92b9..c8754794c6 100644 --- a/packages/opencode/src/cli/cmd/run/otel.ts +++ b/packages/opencode/src/cli/cmd/run/otel.ts @@ -12,14 +12,14 @@ const tracer = trace.getTracer("opencode.run") const runtime = ManagedRuntime.make(Observability.layer, { memoMap }) let ready: Promise | undefined -function attributes(input?: RunSpanAttributes) { +function attributes(input?: RunSpanAttributes): Record | undefined { if (!input) { - return + return undefined } const out = Object.entries(input).flatMap(([key, value]) => (value === undefined ? [] : [[key, value] as const])) if (out.length === 0) { - return + return undefined } return Object.fromEntries(out) diff --git a/packages/opencode/src/cli/cmd/run/permission.shared.ts b/packages/opencode/src/cli/cmd/run/permission.shared.ts index 8f43125846..6ebdbd090c 100644 --- a/packages/opencode/src/cli/cmd/run/permission.shared.ts +++ b/packages/opencode/src/cli/cmd/run/permission.shared.ts @@ -48,7 +48,7 @@ function dict(v: unknown): Dict { return {} } - return v as Dict + return { ...v } } function text(v: unknown): string { @@ -225,7 +225,7 @@ export function permissionRun(state: PermissionBodyState, requestID: string, opt export function permissionReject(state: PermissionBodyState, requestID: string): PermissionReply | undefined { if (state.submitting) { - return + return undefined } return permissionReply(requestID, "reject", state.message) diff --git a/packages/opencode/src/cli/cmd/run/runtime.queue.ts b/packages/opencode/src/cli/cmd/run/runtime.queue.ts index c35f184563..dbc163cd1f 100644 --- a/packages/opencode/src/cli/cmd/run/runtime.queue.ts +++ b/packages/opencode/src/cli/cmd/run/runtime.queue.ts @@ -56,7 +56,7 @@ function defer(): Deferred { // the queue depth so the user knows how many are pending. export async function runPromptQueue(input: QueueInput): Promise { const stop = defer<{ type: "closed" }>() - const done = defer() + const done = defer() const state: State = { queue: [], closed: input.footer.isClosed, diff --git a/packages/opencode/src/cli/cmd/run/runtime.ts b/packages/opencode/src/cli/cmd/run/runtime.ts index df92c7c8fc..7f7589d3dd 100644 --- a/packages/opencode/src/cli/cmd/run/runtime.ts +++ b/packages/opencode/src/cli/cmd/run/runtime.ts @@ -96,9 +96,13 @@ function eagerStream(input: RunRuntimeInput, ctx: BootContext) { return ctx.resume === true || !input.resolveSession || !!input.demo } -async function resolveExitTitle(ctx: BootContext, input: RunRuntimeInput, state: RuntimeState) { +async function resolveExitTitle( + ctx: BootContext, + input: RunRuntimeInput, + state: RuntimeState, +): Promise { if (!state.shown || !hasSession(input, state)) { - return + return undefined } return ctx.sdk.session @@ -267,36 +271,35 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise { }) const footer = shell.footer + const loadCatalog = async (): Promise => { + if (footer.isClosed) { + return + } + + const [agents, resources] = await Promise.all([ + ctx.sdk.app + .agents({ directory: ctx.directory }) + .then((x) => x.data ?? []) + .catch(() => []), + ctx.sdk.experimental.resource + .list({ directory: ctx.directory }) + .then((x) => Object.values(x.data ?? {})) + .catch(() => []), + ]) + if (footer.isClosed) { + return + } + + footer.event({ + type: "catalog", + agents, + resources, + }) + } + void footer .idle() - .then(() => { - if (footer.isClosed) { - return - } - - return Promise.all([ - ctx.sdk.app - .agents({ directory: ctx.directory }) - .then((x) => x.data ?? []) - .catch(() => []), - ctx.sdk.experimental.resource - .list({ directory: ctx.directory }) - .then((x) => Object.values(x.data ?? {})) - .catch(() => []), - ]) - .then(([agents, resources]) => { - if (footer.isClosed) { - return - } - - footer.event({ - type: "catalog", - agents, - resources, - }) - }) - .catch(() => {}) - }) + .then(loadCatalog) .catch(() => {}) if (Flag.OPENCODE_SHOW_TTFD) { @@ -379,7 +382,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise { throw new Error("runtime closed") } - state.selectSubagent = handle.selectSubagent + state.selectSubagent = (sessionID) => handle.selectSubagent(sessionID) return { mod, handle } })() state.stream = next diff --git a/packages/opencode/src/cli/cmd/run/scrollback.surface.ts b/packages/opencode/src/cli/cmd/run/scrollback.surface.ts index 87caf5ef3a..e9b1dc7a92 100644 --- a/packages/opencode/src/cli/cmd/run/scrollback.surface.ts +++ b/packages/opencode/src/cli/cmd/run/scrollback.surface.ts @@ -172,7 +172,11 @@ export class RunScrollbackStream { } if (active.body.type === "text") { - const renderable = active.renderable as TextRenderable + if (!(active.renderable instanceof TextRenderable)) { + return false + } + + const renderable = active.renderable renderable.content = active.content active.surface.render() const targetRows = done ? active.surface.height : Math.max(active.committedRows, active.surface.height - 1) @@ -190,7 +194,11 @@ export class RunScrollbackStream { } if (active.body.type === "code") { - const renderable = active.renderable as CodeRenderable + if (!(active.renderable instanceof CodeRenderable)) { + return false + } + + const renderable = active.renderable renderable.content = active.content renderable.streaming = !done await active.surface.settle() @@ -208,7 +216,11 @@ export class RunScrollbackStream { return true } - const renderable = active.renderable as MarkdownRenderable + if (!(active.renderable instanceof MarkdownRenderable)) { + return false + } + + const renderable = active.renderable renderable.content = active.content renderable.streaming = !done await active.surface.settle() @@ -237,7 +249,7 @@ export class RunScrollbackStream { private async finishActive(trailingNewline: boolean): Promise { if (!this.active) { - return + return undefined } const active = this.active diff --git a/packages/opencode/src/cli/cmd/run/scrollback.writer.tsx b/packages/opencode/src/cli/cmd/run/scrollback.writer.tsx index 2de41c3294..2e3d802955 100644 --- a/packages/opencode/src/cli/cmd/run/scrollback.writer.tsx +++ b/packages/opencode/src/cli/cmd/run/scrollback.writer.tsx @@ -31,7 +31,7 @@ function todoColor(theme: RunTheme, status: string) { export function entryGroupKey(commit: StreamCommit): string | undefined { if (!commit.partID) { - return + return undefined } if (toolStructuredFinal(commit)) { @@ -93,35 +93,19 @@ export function RunEntryContent(props: { const body = createMemo(() => entryBody(props.commit)) const text = () => { const value = body() - if (value.type !== "text") { - return - } - - return value + return value.type === "text" ? value : undefined } const code = () => { const value = body() - if (value.type !== "code") { - return - } - - return value + return value.type === "code" ? value : undefined } const snapshot = () => { const value = body() - if (value.type !== "structured") { - return - } - - return value.snapshot + return value.type === "structured" ? value.snapshot : undefined } const markdown = () => { const value = body() - if (value.type !== "markdown") { - return - } - - return value + return value.type === "markdown" ? value : undefined } if (body().type === "none") { diff --git a/packages/opencode/src/cli/cmd/run/session-data.ts b/packages/opencode/src/cli/cmd/run/session-data.ts index 952eaa3ced..2c04a5752a 100644 --- a/packages/opencode/src/cli/cmd/run/session-data.ts +++ b/packages/opencode/src/cli/cmd/run/session-data.ts @@ -136,7 +136,7 @@ function formatUsage( if (typeof cost === "number" && cost > 0) { return money.format(cost) } - return + return undefined } const text = @@ -157,15 +157,15 @@ export function formatError(error: { } }): string { if (error.data?.message) { - return String(error.data.message) + return error.data.message } if (error.message) { - return String(error.message) + return error.message } if (error.name) { - return String(error.name) + return error.name } return "unknown error" @@ -181,7 +181,7 @@ function msgErr(id: string): string { function patch(patch?: FooterPatch, view?: FooterView): FooterOutput | undefined { if (!patch && !view) { - return + return undefined } return { @@ -262,7 +262,7 @@ function upsert(list: T[], item: T) { list[idx] = item } -function remove(list: T[], id: string): boolean { +function remove(list: Array<{ id: string }>, id: string): boolean { const idx = list.findIndex((entry) => entry.id === id) if (idx === -1) { return false @@ -334,7 +334,7 @@ function enrichPermission(data: SessionData, request: PermissionRequest): Permis function syncPermission(data: SessionData, part: ToolPart): FooterOutput | undefined { data.call.set(key(part.messageID, part.callID), part.state.input) if (data.permissions.length === 0) { - return + return undefined } let changed = false @@ -355,7 +355,7 @@ function syncPermission(data: SessionData, part: ToolPart): FooterOutput | undef }) if (!changed || !active) { - return + return undefined } return { @@ -437,7 +437,7 @@ function stashEcho(data: SessionData, part: ToolPart) { return } - const output = (part.state as { output?: unknown }).output + const output = "output" in part.state ? part.state.output : undefined if (typeof output !== "string") { return } @@ -547,7 +547,7 @@ function drop(data: SessionData, partID: string) { // buffered text parts that were waiting on role confirmation. User-role // parts are silently dropped. function replay(data: SessionData, commits: SessionCommit[], messageID: string, role: MessageRole, thinking: boolean) { - for (const [partID, msg] of [...data.msg.entries()]) { + for (const [partID, msg] of data.msg.entries()) { if (msg !== messageID || data.ids.has(partID)) { continue } @@ -628,7 +628,7 @@ function failTool(part: ToolPart, text: string): SessionCommit { // Emits "interrupted" final entries for all in-flight parts. Called when a turn is aborted. export function flushInterrupted(data: SessionData, commits: SessionCommit[]) { - for (const partID of [...data.part.keys()]) { + for (const partID of data.part.keys()) { if (data.ids.has(partID)) { continue } @@ -689,7 +689,7 @@ export function reduceSessionData(input: SessionDataInput): SessionDataOutput { ) if (usage) { next = { - ...(next ?? {}), + ...next, usage, } } diff --git a/packages/opencode/src/cli/cmd/run/session.shared.ts b/packages/opencode/src/cli/cmd/run/session.shared.ts index 997aa6c9d3..3513d76ab4 100644 --- a/packages/opencode/src/cli/cmd/run/session.shared.ts +++ b/packages/opencode/src/cli/cmd/run/session.shared.ts @@ -68,12 +68,12 @@ function prompt(msg: SessionMessages[number]): RunPrompt { let cursor = Bun.stringWidth(text) const used: Array<{ start: number; end: number }> = [] - const take = (value: string) => { + const take = (value: string): { start: number; end: number; value: string } | undefined => { let from = 0 while (true) { const idx = text.indexOf(value, from) if (idx === -1) { - return + return undefined } const start = Bun.stringWidth(text.slice(0, idx)) @@ -128,7 +128,7 @@ function prompt(msg: SessionMessages[number]): RunPrompt { function turn(msg: SessionMessages[number]): Turn | undefined { if (msg.info.role !== "user") { - return + return undefined } return { @@ -177,7 +177,7 @@ export function sessionHistory(session: RunSession, limit = LIMIT): RunPrompt[] export function sessionVariant(session: RunSession, model: RunInput["model"]): string | undefined { if (!model) { - return + return undefined } for (let idx = session.turns.length - 1; idx >= 0; idx -= 1) { @@ -188,4 +188,6 @@ export function sessionVariant(session: RunSession, model: RunInput["model"]): s return turn.variant } + + return undefined } diff --git a/packages/opencode/src/cli/cmd/run/stream.transport.ts b/packages/opencode/src/cli/cmd/run/stream.transport.ts index 86f388a63a..e8ff21a533 100644 --- a/packages/opencode/src/cli/cmd/run/stream.transport.ts +++ b/packages/opencode/src/cli/cmd/run/stream.transport.ts @@ -35,7 +35,6 @@ import { reduceSubagentData, sameSubagentTab, snapshotSelectedSubagentData, - snapshotSubagentData, SUBAGENT_BOOTSTRAP_LIMIT, SUBAGENT_CALL_BOOTSTRAP_LIMIT, type SubagentData, @@ -135,6 +134,18 @@ function sid(event: Event): string | undefined { ) { return event.properties.sessionID } + + return undefined +} + +function isEvent(value: unknown): value is Event { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return false + } + + const type = Reflect.get(value, "type") + const properties = Reflect.get(value, "properties") + return typeof type === "string" && !!properties && typeof properties === "object" } function active(event: Event, sessionID: string): boolean { @@ -156,7 +167,7 @@ function waitTurn(done: Wait["done"], signal: AbortSignal) { Effect.callback<"abort">((resume) => { if (signal.aborted) { resume(Effect.succeed("abort")) - return + return Effect.void } const onAbort = () => { @@ -243,23 +254,23 @@ function composeFooter(input: { if (input.subagent) { footer = { - ...(footer ?? {}), + ...footer, subagent: input.subagent, } } if (!sameView(input.previous, input.current)) { footer = { - ...(footer ?? {}), + ...footer, view: input.current, } } if (input.current.type !== "prompt") { footer = { - ...(footer ?? {}), + ...footer, patch: { - ...(input.patch ?? {}), + ...input.patch, status: blockerStatus(input.current), }, } @@ -268,7 +279,7 @@ function composeFooter(input: { if (input.patch) { footer = { - ...(footer ?? {}), + ...footer, patch: input.patch, } return footer @@ -276,7 +287,7 @@ function composeFooter(input: { if (input.previous.type !== "prompt") { footer = { - ...(footer ?? {}), + ...footer, patch: { status: "", }, @@ -622,7 +633,11 @@ function createLayer(input: StreamInput) { return } - const event = item as Event + if (!isEvent(item)) { + return + } + + const event = item input.trace?.write("recv.event", event) trackBlocker(event) @@ -675,11 +690,13 @@ function createLayer(input: StreamInput) { } if (state.fault) { - return yield* Effect.fail(state.fault) + yield* Effect.fail(state.fault) + return } if (state.wait) { - return yield* Effect.fail(new Error("prompt already running")) + yield* Effect.fail(new Error("prompt already running")) + return } const prev = listSubagentTabs(state.subagent) @@ -733,7 +750,7 @@ function createLayer(input: StreamInput) { ), ) - return yield* send.pipe( + yield* send.pipe( Effect.flatMap(() => { if (turn.signal.aborted || next.signal?.aborted || input.footer.isClosed || closed) { if (state.wait === item) { @@ -805,6 +822,7 @@ function createLayer(input: StreamInput) { }), ), ) + return }) const selectSubagent = Effect.fn("RunStreamTransport.selectSubagent")((sessionID: string | undefined) => diff --git a/packages/opencode/src/cli/cmd/run/subagent-data.ts b/packages/opencode/src/cli/cmd/run/subagent-data.ts index 628b2084aa..e834ff74f0 100644 --- a/packages/opencode/src/cli/cmd/run/subagent-data.ts +++ b/packages/opencode/src/cli/cmd/run/subagent-data.ts @@ -117,24 +117,24 @@ function sameCommit(left: StreamCommit, right: StreamCommit) { ) } -function text(value: unknown) { +function text(value: unknown): string | undefined { if (typeof value !== "string") { - return + return undefined } const next = value.trim() return next || undefined } -function num(value: unknown) { +function num(value: unknown): number | undefined { if (typeof value === "number" && Number.isFinite(value)) { return value } - return + return undefined } -function inputLabel(input: Record) { +function inputLabel(input: Record): string | undefined { const description = text(input.description) if (description) { return description @@ -175,21 +175,60 @@ function inputLabel(input: Record) { return prompt } - return + return undefined } function stateTitle(part: ToolPart) { return text("title" in part.state ? part.state.title : undefined) } -function callKey(messageID: string | undefined, callID: string | undefined) { +function callKey(messageID: string | undefined, callID: string | undefined): string | undefined { if (!messageID || !callID) { - return + return undefined } return `${messageID}:${callID}` } +function compactToolState(part: ToolPart): ToolPart["state"] { + if (part.state.status === "pending") { + return { + status: "pending", + input: part.state.input, + raw: part.state.raw, + } + } + + if (part.state.status === "running") { + return { + status: "running", + input: part.state.input, + time: part.state.time, + ...(part.state.metadata ? { metadata: part.state.metadata } : {}), + ...(part.state.title ? { title: part.state.title } : {}), + } + } + + if (part.state.status === "completed") { + return { + status: "completed", + input: part.state.input, + output: part.state.output, + title: part.state.title, + metadata: part.state.metadata, + time: part.state.time, + } + } + + return { + status: "error", + input: part.state.input, + error: part.state.error, + time: part.state.time, + ...(part.state.metadata ? { metadata: part.state.metadata } : {}), + } +} + function recent(input: Iterable, limit: number) { const list = [...input] return list.slice(Math.max(0, list.length - limit)) @@ -215,15 +254,9 @@ function compactToolPart(part: ToolPart): ToolPart { messageID: part.messageID, callID: part.callID, tool: part.tool, - state: { - status: part.state.status, - input: part.state.input, - metadata: "metadata" in part.state ? part.state.metadata : undefined, - time: "time" in part.state ? part.state.time : undefined, - title: "title" in part.state ? part.state.title : undefined, - error: "error" in part.state ? part.state.error : undefined, - }, - } as ToolPart + state: compactToolState(part), + ...(part.metadata ? { metadata: part.metadata } : {}), + } } function compactCommit(commit: StreamCommit): StreamCommit { @@ -623,7 +656,7 @@ export function bootstrapSubagentCalls(input: { data: SubagentData; sessionID: s export function clearFinishedSubagents(data: SubagentData) { let changed = false - for (const [sessionID, tab] of [...data.tabs.entries()]) { + for (const [sessionID, tab] of data.tabs.entries()) { if (tab.status === "running") { continue } diff --git a/packages/opencode/src/cli/cmd/run/theme.ts b/packages/opencode/src/cli/cmd/run/theme.ts index 4993a04cf1..00bf029003 100644 --- a/packages/opencode/src/cli/cmd/run/theme.ts +++ b/packages/opencode/src/cli/cmd/run/theme.ts @@ -114,7 +114,7 @@ function blend(color: RGBA, bg: RGBA): RGBA { export function opaqueSyntaxStyle(style: SyntaxStyle | undefined, bg: RGBA): SyntaxStyle | undefined { if (!style) { - return + return undefined } return SyntaxStyle.fromStyles( diff --git a/packages/opencode/src/cli/cmd/run/tool.ts b/packages/opencode/src/cli/cmd/run/tool.ts index d46447768b..aa4f44d940 100644 --- a/packages/opencode/src/cli/cmd/run/tool.ts +++ b/packages/opencode/src/cli/cmd/run/tool.ts @@ -130,28 +130,28 @@ type ToolRegistry = { [K in ToolName]: ToolRule } -type AnyToolRule = ToolRule +type AnyToolRule = ToolRule function dict(v: unknown): ToolDict { if (!v || typeof v !== "object" || Array.isArray(v)) { return {} } - return v as ToolDict + return { ...v } } function props(frame: ToolFrame): ToolProps { return { - input: frame.input as Partial>, - metadata: frame.meta as Partial>, + input: Object.assign(Object.create(null), frame.input), + metadata: Object.assign(Object.create(null), frame.meta), frame, } } function permission(ctx: ToolPermissionCtx): ToolPermissionProps { return { - input: ctx.input as Partial>, - metadata: ctx.meta as Partial>, + input: Object.assign(Object.create(null), ctx.input), + metadata: Object.assign(Object.create(null), ctx.meta), patterns: ctx.patterns, } } @@ -162,14 +162,18 @@ function text(v: unknown): string { function num(v: unknown): number | undefined { if (typeof v !== "number" || !Number.isFinite(v)) { - return + return undefined } return v } function list(v: unknown): T[] { - return Array.isArray(v) ? (v as T[]) : [] + if (!Array.isArray(v)) { + return [] + } + + return v } function done(name: string, time: string): string { @@ -193,7 +197,7 @@ function info(data: ToolDict, skip: string[] = []): string { return "" } - return `[${list.map(([key, val]) => `${key}=${val}`).join(", ")}]` + return `[${list.map(([key, val]) => `${key}=${String(val)}`).join(", ")}]` } function span(state: ToolDict): string { @@ -506,7 +510,7 @@ function snapWrite(p: ToolProps): ToolSnapshot | undefined { const file = p.input.filePath || "" const content = p.input.content || "" if (!file && !content) { - return + return undefined } return { @@ -521,7 +525,7 @@ function snapEdit(p: ToolProps): ToolSnapshot | undefined { const file = p.input.filePath || "" const diff = p.metadata.diff || "" if (!file || !diff.trim()) { - return + return undefined } return { @@ -539,31 +543,31 @@ function snapEdit(p: ToolProps): ToolSnapshot | undefined { function snapPatch(p: ToolProps): ToolSnapshot | undefined { const files = list(p.frame.meta.files) if (files.length === 0) { - return + return undefined } return { kind: "diff", - items: files - .map((file) => { - if (!file || typeof file !== "object") { - return - } + items: files.flatMap((file) => { + if (!file || typeof file !== "object") { + return [] + } - const diff = typeof file.patch === "string" ? file.patch : "" - if (!diff.trim()) { - return - } + const diff = typeof file.patch === "string" ? file.patch : "" + if (!diff.trim()) { + return [] + } - const name = file.movePath || file.filePath || file.relativePath - return { + const name = file.movePath || file.filePath || file.relativePath + return [ + { title: patchTitle(file), diff, file: name, deletions: typeof file.deletions === "number" ? file.deletions : 0, - } - }) - .filter((item): item is NonNullable => Boolean(item)), + }, + ] + }), } } @@ -746,9 +750,9 @@ function scrollTaskStart(_: ToolProps): string { return "" } -function taskResult(output: string) { +function taskResult(output: string): string | undefined { if (!output.trim()) { - return + return undefined } const match = output.match(/\s*([\s\S]*?)\s*<\/task_result>/) @@ -1236,10 +1240,10 @@ function key(name: string): name is ToolName { function rule(name?: string): AnyToolRule | undefined { if (!name || !key(name)) { - return + return undefined } - return TOOL_RULES[name] as AnyToolRule + return TOOL_RULES[name] } function frame(part: ToolPart): ToolFrame { @@ -1345,13 +1349,13 @@ export function toolPermissionInfo( ): ToolPermissionInfo | undefined { const draw = rule(name)?.permission if (!draw) { - return + return undefined } try { return draw(permission({ input, meta, patterns })) } catch { - return + return undefined } } @@ -1359,19 +1363,19 @@ export function toolSnapshot(commit: StreamCommit, raw: string): ToolSnapshot | const ctx = toolFrame(commit, raw) const draw = rule(ctx.name)?.snap if (!draw) { - return + return undefined } try { return draw(props(ctx)) } catch { - return + return undefined } } function textBody(content: string): RunEntryBody | undefined { if (!content) { - return + return undefined } return { @@ -1382,7 +1386,7 @@ function textBody(content: string): RunEntryBody | undefined { function markdownBody(content: string): RunEntryBody | undefined { if (!content) { - return + return undefined } return { @@ -1394,7 +1398,7 @@ function markdownBody(content: string): RunEntryBody | undefined { function structuredBody(commit: StreamCommit, raw: string): RunEntryBody | undefined { const snap = toolSnapshot(commit, raw) if (!snap) { - return + return undefined } return { @@ -1409,7 +1413,7 @@ export function toolEntryBody(commit: StreamCommit, raw: string): RunEntryBody | if (ctx.name === "task") { if (commit.phase === "start") { - return + return undefined } if (commit.phase === "final" && ctx.status === "completed") { @@ -1421,7 +1425,7 @@ export function toolEntryBody(commit: StreamCommit, raw: string): RunEntryBody | } if (commit.phase === "progress" && !view.output) { - return + return undefined } if (commit.phase === "final") { @@ -1430,7 +1434,7 @@ export function toolEntryBody(commit: StreamCommit, raw: string): RunEntryBody | } if (!view.final) { - return + return undefined } if (ctx.status && ctx.status !== "completed") { @@ -1447,7 +1451,7 @@ export function toolEntryBody(commit: StreamCommit, raw: string): RunEntryBody | export function toolFiletype(input?: string): string | undefined { if (!input) { - return + return undefined } const ext = path.extname(input) diff --git a/packages/opencode/src/cli/cmd/run/trace.ts b/packages/opencode/src/cli/cmd/run/trace.ts index 8770d33444..2127b46057 100644 --- a/packages/opencode/src/cli/cmd/run/trace.ts +++ b/packages/opencode/src/cli/cmd/run/trace.ts @@ -50,14 +50,14 @@ function text(data: unknown) { ) } -export function trace() { +export function trace(): Trace | undefined { if (state !== undefined) { return state || undefined } if (!process.env.OPENCODE_DIRECT_TRACE) { state = false - return + return undefined } const target = file() diff --git a/packages/opencode/src/cli/cmd/run/variant.shared.ts b/packages/opencode/src/cli/cmd/run/variant.shared.ts index 7ed8c35289..a5bf47aa7d 100644 --- a/packages/opencode/src/cli/cmd/run/variant.shared.ts +++ b/packages/opencode/src/cli/cmd/run/variant.shared.ts @@ -132,7 +132,7 @@ function createLayer(fs = AppFileSystem.defaultLayer) { const read = Effect.fn("RunVariant.read")(function* () { return yield* file.readJson(MODEL_FILE).pipe( Effect.map(state), - Effect.catchCause(() => Effect.succeed({})), + Effect.catchCause(() => Effect.succeed(state(undefined))), ) }) @@ -154,7 +154,7 @@ function createLayer(fs = AppFileSystem.defaultLayer) { const current = yield* read() const next = { - ...(current.variant ?? {}), + ...current.variant, } const key = variantKey(model) if (variant) {