Pārlūkot izejas kodu

fix linter errors

Simon Klee 5 dienas atpakaļ
vecāks
revīzija
f686455ce3

+ 51 - 35
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<void> {
@@ -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: "",

+ 1 - 1
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
           }}
         >
           <box width="100%" flexDirection="column" gap={0}>

+ 6 - 4
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<FooterView>({ type: "prompt" })
     this.view = view
     this.setView = setView
-    const [agents, setAgents] = createSignal<RunAgent[]>(options.agents)
+    const [agents, setAgents] = createSignal(options.agents)
     this.agents = agents
     this.setAgents = setAgents
-    const [resources, setResources] = createSignal<RunResource[]>(options.resources)
+    const [resources, setResources] = createSignal(options.resources)
     this.resources = resources
     this.setResources = setResources
     const [subagent, setSubagent] = createStore<FooterSubagentState>(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<typeof render>[1],
+      this.renderer,
     ).catch(() => {
       if (!this.isGone) {
         this.close()

+ 3 - 7
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))

+ 3 - 3
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<void> | undefined
 
-function attributes(input?: RunSpanAttributes) {
+function attributes(input?: RunSpanAttributes): Record<string, string | number | boolean> | 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)

+ 2 - 2
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)

+ 1 - 1
packages/opencode/src/cli/cmd/run/runtime.queue.ts

@@ -56,7 +56,7 @@ function defer<T = void>(): Deferred<T> {
 // the queue depth so the user knows how many are pending.
 export async function runPromptQueue(input: QueueInput): Promise<void> {
   const stop = defer<{ type: "closed" }>()
-  const done = defer<void>()
+  const done = defer()
   const state: State = {
     queue: [],
     closed: input.footer.isClosed,

+ 34 - 31
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<string | undefined> {
   if (!state.shown || !hasSession(input, state)) {
-    return
+    return undefined
   }
 
   return ctx.sdk.session
@@ -267,36 +271,35 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
       })
       const footer = shell.footer
 
-      void footer
-        .idle()
-        .then(() => {
-          if (footer.isClosed) {
-            return
-          }
+      const loadCatalog = async (): Promise<void> => {
+        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(() => {})
+        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(loadCatalog)
         .catch(() => {})
 
       if (Flag.OPENCODE_SHOW_TTFD) {
@@ -379,7 +382,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
             throw new Error("runtime closed")
           }
 
-          state.selectSubagent = handle.selectSubagent
+          state.selectSubagent = (sessionID) => handle.selectSubagent(sessionID)
           return { mod, handle }
         })()
         state.stream = next

+ 16 - 4
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<StreamCommit | undefined> {
     if (!this.active) {
-      return
+      return undefined
     }
 
     const active = this.active

+ 5 - 21
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") {

+ 12 - 12
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<T extends { id: string }>(list: T[], item: T) {
   list[idx] = item
 }
 
-function remove<T extends { id: string }>(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,
       }
     }

+ 6 - 4
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
 }

+ 30 - 12
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) =>

+ 51 - 18
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<string, unknown>) {
+function inputLabel(input: Record<string, unknown>): string | undefined {
   const description = text(input.description)
   if (description) {
     return description
@@ -175,21 +175,60 @@ function inputLabel(input: Record<string, unknown>) {
     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<T>(input: Iterable<T>, 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
     }

+ 1 - 1
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(

+ 45 - 41
packages/opencode/src/cli/cmd/run/tool.ts

@@ -130,28 +130,28 @@ type ToolRegistry = {
   [K in ToolName]: ToolRule<ToolDefs[K]>
 }
 
-type AnyToolRule = ToolRule<Tool.Info>
+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<T = Tool.Info>(frame: ToolFrame): ToolProps<T> {
   return {
-    input: frame.input as Partial<Tool.InferParameters<T>>,
-    metadata: frame.meta as Partial<Tool.InferMetadata<T>>,
+    input: Object.assign(Object.create(null), frame.input),
+    metadata: Object.assign(Object.create(null), frame.meta),
     frame,
   }
 }
 
 function permission<T = Tool.Info>(ctx: ToolPermissionCtx): ToolPermissionProps<T> {
   return {
-    input: ctx.input as Partial<Tool.InferParameters<T>>,
-    metadata: ctx.meta as Partial<Tool.InferMetadata<T>>,
+    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<T>(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<typeof WriteTool>): 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<typeof EditTool>): 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<typeof EditTool>): ToolSnapshot | undefined {
 function snapPatch(p: ToolProps<typeof ApplyPatchTool>): ToolSnapshot | undefined {
   const files = list<PatchFile>(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<typeof item> => Boolean(item)),
+        },
+      ]
+    }),
   }
 }
 
@@ -746,9 +750,9 @@ function scrollTaskStart(_: ToolProps<typeof TaskTool>): string {
   return ""
 }
 
-function taskResult(output: string) {
+function taskResult(output: string): string | undefined {
   if (!output.trim()) {
-    return
+    return undefined
   }
 
   const match = output.match(/<task_result>\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)

+ 2 - 2
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()

+ 2 - 2
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) {