From 6f5a3d30fddbe09e555fb1c27b6a63551a73de36 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Fri, 10 Apr 2026 09:34:01 +0530 Subject: [PATCH 001/335] keep recent turns during session compaction --- .../opencode/src/agent/prompt/compaction.txt | 3 +- packages/opencode/src/config/config.ts | 12 + packages/opencode/src/session/compaction.ts | 90 ++++- packages/opencode/src/session/message-v2.ts | 20 +- .../opencode/test/session/compaction.test.ts | 341 +++++++++++++++++- .../test/session/messages-pagination.test.ts | 136 ++++++- 6 files changed, 587 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/agent/prompt/compaction.txt b/packages/opencode/src/agent/prompt/compaction.txt index 11deccb3af..c5831bb30e 100644 --- a/packages/opencode/src/agent/prompt/compaction.txt +++ b/packages/opencode/src/agent/prompt/compaction.txt @@ -1,6 +1,7 @@ You are a helpful AI assistant tasked with summarizing conversations. -When asked to summarize, provide a detailed but concise summary of the conversation. +When asked to summarize, provide a detailed but concise summary of the older conversation history. +The most recent turns may be preserved verbatim outside your summary, so focus on information that would still be needed to continue the work with that recent context available. Focus on information that would be helpful for continuing the conversation, including: - What was done - What is currently being worked on diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ff79b739fe..f48e2e8d2c 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1069,6 +1069,18 @@ export namespace Config { .object({ auto: z.boolean().optional().describe("Enable automatic compaction when context is full (default: true)"), prune: z.boolean().optional().describe("Enable pruning of old tool outputs (default: true)"), + tail_turns: z + .number() + .int() + .min(0) + .optional() + .describe("Number of recent real user turns to keep verbatim during compaction (default: 2)"), + tail_tokens: z + .number() + .int() + .min(0) + .optional() + .describe("Token budget for retained recent turns during compaction"), reserved: z .number() .int() diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 937aa71325..69ecc44ba2 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -4,6 +4,7 @@ import { Session } from "." import { SessionID, MessageID, PartID } from "./schema" import { Instance } from "../project/instance" import { Provider } from "../provider/provider" +import { ProviderTransform } from "../provider/transform" import { MessageV2 } from "./message-v2" import z from "zod" import { Token } from "../util/token" @@ -35,6 +36,24 @@ export namespace SessionCompaction { export const PRUNE_MINIMUM = 20_000 export const PRUNE_PROTECT = 40_000 const PRUNE_PROTECTED_TOOLS = ["skill"] + const DEFAULT_TAIL_TURNS = 2 + const MIN_TAIL_TOKENS = 2_000 + const MAX_TAIL_TOKENS = 8_000 + + function usable(input: { cfg: Config.Info; model: Provider.Model }) { + const reserved = + input.cfg.compaction?.reserved ?? Math.min(20_000, ProviderTransform.maxOutputTokens(input.model)) + return input.model.limit.input + ? Math.max(0, input.model.limit.input - reserved) + : Math.max(0, input.model.limit.context - ProviderTransform.maxOutputTokens(input.model)) + } + + function tailBudget(input: { cfg: Config.Info; model: Provider.Model }) { + return ( + input.cfg.compaction?.tail_tokens ?? + Math.min(MAX_TAIL_TOKENS, Math.max(MIN_TAIL_TOKENS, Math.floor(usable(input) * 0.25))) + ) + } export interface Interface { readonly isOverflow: (input: { @@ -88,6 +107,55 @@ export namespace SessionCompaction { return overflow({ cfg: yield* config.get(), tokens: input.tokens, model: input.model }) }) + const estimate = Effect.fn("SessionCompaction.estimate")(function* (input: { + messages: MessageV2.WithParts[] + model: Provider.Model + }) { + const msgs = yield* MessageV2.toModelMessagesEffect(input.messages, input.model, { stripMedia: true }) + return Token.estimate(JSON.stringify(msgs)) + }) + + const select = Effect.fn("SessionCompaction.select")(function* (input: { + messages: MessageV2.WithParts[] + cfg: Config.Info + model: Provider.Model + }) { + const limit = input.cfg.compaction?.tail_turns ?? DEFAULT_TAIL_TURNS + if (limit <= 0) return { head: input.messages, tail_start_id: undefined } + const budget = tailBudget({ cfg: input.cfg, model: input.model }) + const turns = input.messages.flatMap((msg, idx) => + msg.info.role === "user" && !msg.parts.some((part) => part.type === "compaction") ? [idx] : [], + ) + if (!turns.length) return { head: input.messages, tail_start_id: undefined } + + let total = 0 + let start = input.messages.length + let kept = 0 + + for (let i = turns.length - 1; i >= 0 && kept < limit; i--) { + const idx = turns[i] + const end = i + 1 < turns.length ? turns[i + 1] : input.messages.length + const size = yield* estimate({ + messages: input.messages.slice(idx, end), + model: input.model, + }) + if (kept === 0 && size > budget) { + log.info("tail fallback", { budget, size }) + return { head: input.messages, tail_start_id: undefined } + } + if (total + size > budget) break + total += size + start = idx + kept++ + } + + if (kept === 0 || start === 0) return { head: input.messages, tail_start_id: undefined } + return { + head: input.messages.slice(0, start), + tail_start_id: input.messages[start]?.info.id, + } + }) + // goes backwards through parts until there are PRUNE_PROTECT tokens worth of tool // calls, then erases output of older tool calls to free context space const prune = Effect.fn("SessionCompaction.prune")(function* (input: { sessionID: SessionID }) { @@ -150,6 +218,7 @@ export namespace SessionCompaction { throw new Error(`Compaction parent must be a user message: ${input.parentID}`) } const userMessage = parent.info + const compactionPart = parent.parts.find((part): part is MessageV2.CompactionPart => part.type === "compaction") let messages = input.messages let replay: @@ -180,14 +249,22 @@ export namespace SessionCompaction { const model = agent.model ? yield* provider.getModel(agent.model.providerID, agent.model.modelID) : yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID) + const cfg = yield* config.get() + const history = compactionPart && messages.at(-1)?.info.id === input.parentID ? messages.slice(0, -1) : messages + const selected = yield* select({ + messages: history, + cfg, + model, + }) // Allow plugins to inject context or replace compaction prompt. const compacting = yield* plugin.trigger( "experimental.session.compacting", { sessionID: input.sessionID }, { context: [], prompt: undefined }, ) - const defaultPrompt = `Provide a detailed prompt for continuing our conversation above. -Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next. + const defaultPrompt = `Summarize the older conversation history so another agent can continue the work with the retained recent turns. +The most recent conversation turns will remain verbatim outside this summary, so focus on older context that is still needed to understand and continue the work. +Include what we did, what we're doing, which files we're working on, and what we're going to do next. The summary that you construct will be used so that another agent can read it and continue the work. Do not call any tools. Respond only with the summary text. Respond in the same language as the user's messages in the conversation. @@ -217,7 +294,7 @@ When constructing the summary, try to stick to this template: ---` const prompt = compacting.prompt ?? [defaultPrompt, ...compacting.context].join("\n\n") - const msgs = structuredClone(messages) + const msgs = structuredClone(selected.head) yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs }) const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, { stripMedia: true }) const ctx = yield* InstanceState.context @@ -280,6 +357,13 @@ When constructing the summary, try to stick to this template: return "stop" } + if (compactionPart && selected.tail_start_id && compactionPart.tail_start_id !== selected.tail_start_id) { + yield* session.updatePart({ + ...compactionPart, + tail_start_id: selected.tail_start_id, + }) + } + if (result === "continue" && input.auto) { if (replay) { const original = replay.info diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 61c159646d..0f9e91d9b9 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -208,6 +208,7 @@ export namespace MessageV2 { type: z.literal("compaction"), auto: z.boolean(), overflow: z.boolean().optional(), + tail_start_id: MessageID.zod.optional(), }).meta({ ref: "CompactionPart", }) @@ -926,14 +927,21 @@ export namespace MessageV2 { export function filterCompacted(msgs: Iterable) { const result = [] as MessageV2.WithParts[] const completed = new Set() + let retain: MessageID | undefined for (const msg of msgs) { result.push(msg) - if ( - msg.info.role === "user" && - completed.has(msg.info.id) && - msg.parts.some((part) => part.type === "compaction") - ) - break + if (retain) { + if (msg.info.id === retain) break + continue + } + if (msg.info.role === "user" && completed.has(msg.info.id)) { + const part = msg.parts.find((item): item is MessageV2.CompactionPart => item.type === "compaction") + if (!part) continue + if (!part.tail_start_id) break + retain = part.tail_start_id + if (msg.info.id === retain) break + continue + } if (msg.info.role === "assistant" && msg.info.summary && msg.info.finish && !msg.info.error) completed.add(msg.info.parentID) } diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 76a83c34da..1676b1ef60 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -154,7 +154,19 @@ function layer(result: "continue" | "compact") { ) } -function runtime(result: "continue" | "compact", plugin = Plugin.defaultLayer, provider = ProviderTest.fake()) { +function cfg(compaction?: Config.Info["compaction"]) { + const base = Config.Info.parse({}) + return Layer.mock(Config.Service)({ + get: () => Effect.succeed({ ...base, compaction }), + }) +} + +function runtime( + result: "continue" | "compact", + plugin = Plugin.defaultLayer, + provider = ProviderTest.fake(), + config = Config.defaultLayer, +) { const bus = Bus.layer return ManagedRuntime.make( Layer.mergeAll(SessionCompaction.layer, bus).pipe( @@ -164,7 +176,7 @@ function runtime(result: "continue" | "compact", plugin = Plugin.defaultLayer, p Layer.provide(Agent.defaultLayer), Layer.provide(plugin), Layer.provide(bus), - Layer.provide(Config.defaultLayer), + Layer.provide(config), ), ) } @@ -191,7 +203,7 @@ function llm() { } } -function liveRuntime(layer: Layer.Layer, provider = ProviderTest.fake()) { +function liveRuntime(layer: Layer.Layer, provider = ProviderTest.fake(), config = Config.defaultLayer) { const bus = Bus.layer const status = SessionStatus.layer.pipe(Layer.provide(bus)) const processor = SessionProcessorModule.SessionProcessor.layer @@ -206,11 +218,66 @@ function liveRuntime(layer: Layer.Layer, provider = ProviderTest.fa Layer.provide(Plugin.defaultLayer), Layer.provide(status), Layer.provide(bus), - Layer.provide(Config.defaultLayer), + Layer.provide(config), ), ) } +function reply( + text: string, + capture?: (input: LLM.StreamInput) => void, +): (input: LLM.StreamInput) => Stream.Stream { + return (input) => { + capture?.(input) + return Stream.make( + { type: "start" } satisfies LLM.Event, + { type: "text-start", id: "txt-0" } satisfies LLM.Event, + { type: "text-delta", id: "txt-0", delta: text, text } as LLM.Event, + { type: "text-end", id: "txt-0" } satisfies LLM.Event, + { + type: "finish-step", + finishReason: "stop", + rawFinishReason: "stop", + response: { id: "res", modelId: "test-model", timestamp: new Date() }, + providerMetadata: undefined, + usage: { + inputTokens: 1, + outputTokens: 1, + totalTokens: 2, + inputTokenDetails: { + noCacheTokens: undefined, + cacheReadTokens: undefined, + cacheWriteTokens: undefined, + }, + outputTokenDetails: { + textTokens: undefined, + reasoningTokens: undefined, + }, + }, + } satisfies LLM.Event, + { + type: "finish", + finishReason: "stop", + rawFinishReason: "stop", + totalUsage: { + inputTokens: 1, + outputTokens: 1, + totalTokens: 2, + inputTokenDetails: { + noCacheTokens: undefined, + cacheReadTokens: undefined, + cacheWriteTokens: undefined, + }, + outputTokenDetails: { + textTokens: undefined, + reasoningTokens: undefined, + }, + }, + } satisfies LLM.Event, + ) + } +} + function wait(ms = 50) { return new Promise((resolve) => setTimeout(resolve, ms)) } @@ -661,6 +728,148 @@ describe("session.compaction.process", () => { }) }) + test("persists tail_start_id for retained recent turns", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + await user(session.id, "first") + const keep = await user(session.id, "second") + await user(session.id, "third") + await SessionCompaction.create({ + sessionID: session.id, + agent: "build", + model: ref, + auto: false, + }) + + const rt = runtime("continue", Plugin.defaultLayer, wide(), cfg({ tail_turns: 2, tail_tokens: 10_000 })) + try { + const msgs = await Session.messages({ sessionID: session.id }) + const parent = msgs.at(-1)?.info.id + expect(parent).toBeTruthy() + await rt.runPromise( + SessionCompaction.Service.use((svc) => + svc.process({ + parentID: parent!, + messages: msgs, + sessionID: session.id, + auto: false, + }), + ), + ) + + const part = (await Session.messages({ sessionID: session.id })).at(-2)?.parts.find( + (item) => item.type === "compaction", + ) + + expect(part?.type).toBe("compaction") + if (part?.type === "compaction") expect(part.tail_start_id).toBe(keep.id) + } finally { + await rt.dispose() + } + }, + }) + }) + + test("shrinks retained tail to fit tail token budget", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + await user(session.id, "first") + await user(session.id, "x".repeat(2_000)) + const keep = await user(session.id, "tiny") + await SessionCompaction.create({ + sessionID: session.id, + agent: "build", + model: ref, + auto: false, + }) + + const rt = runtime("continue", Plugin.defaultLayer, wide(), cfg({ tail_turns: 2, tail_tokens: 100 })) + try { + const msgs = await Session.messages({ sessionID: session.id }) + const parent = msgs.at(-1)?.info.id + expect(parent).toBeTruthy() + await rt.runPromise( + SessionCompaction.Service.use((svc) => + svc.process({ + parentID: parent!, + messages: msgs, + sessionID: session.id, + auto: false, + }), + ), + ) + + const part = (await Session.messages({ sessionID: session.id })).at(-2)?.parts.find( + (item) => item.type === "compaction", + ) + + expect(part?.type).toBe("compaction") + if (part?.type === "compaction") expect(part.tail_start_id).toBe(keep.id) + } finally { + await rt.dispose() + } + }, + }) + }) + + test("falls back to full summary when even one recent turn exceeds tail budget", async () => { + await using tmp = await tmpdir({ git: true }) + const stub = llm() + let captured = "" + stub.push( + reply("summary", (input) => { + captured = JSON.stringify(input.messages) + }), + ) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + await user(session.id, "first") + await user(session.id, "y".repeat(2_000)) + await SessionCompaction.create({ + sessionID: session.id, + agent: "build", + model: ref, + auto: false, + }) + + const rt = liveRuntime(stub.layer, wide(), cfg({ tail_turns: 1, tail_tokens: 20 })) + try { + const msgs = await Session.messages({ sessionID: session.id }) + const parent = msgs.at(-1)?.info.id + expect(parent).toBeTruthy() + await rt.runPromise( + SessionCompaction.Service.use((svc) => + svc.process({ + parentID: parent!, + messages: msgs, + sessionID: session.id, + auto: false, + }), + ), + ) + + const part = (await Session.messages({ sessionID: session.id })).at(-2)?.parts.find( + (item) => item.type === "compaction", + ) + + expect(part?.type).toBe("compaction") + if (part?.type === "compaction") expect(part.tail_start_id).toBeUndefined() + expect(captured).toContain("yyyy") + } finally { + await rt.dispose() + } + }, + }) + }) + test("replays the prior user turn on overflow when earlier context exists", async () => { await using tmp = await tmpdir() await Instance.provide({ @@ -978,6 +1187,130 @@ describe("session.compaction.process", () => { }, }) }) + + test("summarizes only the head while keeping recent tail out of summary input", async () => { + const stub = llm() + let captured = "" + stub.push( + reply("summary", (input) => { + captured = JSON.stringify(input.messages) + }), + ) + + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + await user(session.id, "older context") + await user(session.id, "keep this turn") + await user(session.id, "and this one too") + await SessionCompaction.create({ + sessionID: session.id, + agent: "build", + model: ref, + auto: false, + }) + + const rt = liveRuntime(stub.layer, wide()) + try { + const msgs = await Session.messages({ sessionID: session.id }) + const parent = msgs.at(-1)?.info.id + expect(parent).toBeTruthy() + await rt.runPromise( + SessionCompaction.Service.use((svc) => + svc.process({ + parentID: parent!, + messages: msgs, + sessionID: session.id, + auto: false, + }), + ), + ) + + expect(captured).toContain("older context") + expect(captured).not.toContain("keep this turn") + expect(captured).not.toContain("and this one too") + expect(captured).not.toContain("What did we do so far?") + } finally { + await rt.dispose() + } + }, + }) + }) + + test("keeps recent pre-compaction turns across repeated compactions", async () => { + const stub = llm() + stub.push(reply("summary one")) + stub.push(reply("summary two")) + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + const u1 = await user(session.id, "one") + const u2 = await user(session.id, "two") + const u3 = await user(session.id, "three") + await SessionCompaction.create({ + sessionID: session.id, + agent: "build", + model: ref, + auto: false, + }) + + const rt = liveRuntime(stub.layer, wide(), cfg({ tail_turns: 2, tail_tokens: 10_000 })) + try { + let msgs = await Session.messages({ sessionID: session.id }) + let parent = msgs.at(-1)?.info.id + expect(parent).toBeTruthy() + await rt.runPromise( + SessionCompaction.Service.use((svc) => + svc.process({ + parentID: parent!, + messages: msgs, + sessionID: session.id, + auto: false, + }), + ), + ) + + const u4 = await user(session.id, "four") + await SessionCompaction.create({ + sessionID: session.id, + agent: "build", + model: ref, + auto: false, + }) + + msgs = MessageV2.filterCompacted(MessageV2.stream(session.id)) + parent = msgs.at(-1)?.info.id + expect(parent).toBeTruthy() + await rt.runPromise( + SessionCompaction.Service.use((svc) => + svc.process({ + parentID: parent!, + messages: msgs, + sessionID: session.id, + auto: false, + }), + ), + ) + + const filtered = MessageV2.filterCompacted(MessageV2.stream(session.id)) + const ids = filtered.map((msg) => msg.info.id) + + expect(ids).not.toContain(u1.id) + expect(ids).not.toContain(u2.id) + expect(ids).toContain(u3.id) + expect(ids).toContain(u4.id) + expect(filtered.some((msg) => msg.info.role === "assistant" && msg.info.summary)).toBe(true) + expect(filtered.some((msg) => msg.info.role === "user" && msg.parts.some((part) => part.type === "compaction"))).toBe(true) + } finally { + await rt.dispose() + } + }, + }) + }) }) describe("util.token.estimate", () => { diff --git a/packages/opencode/test/session/messages-pagination.test.ts b/packages/opencode/test/session/messages-pagination.test.ts index bb9df6aea3..aea0c6de31 100644 --- a/packages/opencode/test/session/messages-pagination.test.ts +++ b/packages/opencode/test/session/messages-pagination.test.ts @@ -86,13 +86,14 @@ async function addAssistant( return id } -async function addCompactionPart(sessionID: SessionID, messageID: MessageID) { +async function addCompactionPart(sessionID: SessionID, messageID: MessageID, tailStartID?: MessageID) { await Session.updatePart({ id: PartID.ascending(), sessionID, messageID, type: "compaction", auto: true, + tail_start_id: tailStartID, } as any) } @@ -759,6 +760,139 @@ describe("MessageV2.filterCompacted", () => { }) }) + test("retains original tail when compaction stores tail_start_id", async () => { + await Instance.provide({ + directory: root, + fn: async () => { + const session = await Session.create({}) + + const u1 = await addUser(session.id, "first") + const a1 = await addAssistant(session.id, u1, { finish: "end_turn" }) + await Session.updatePart({ + id: PartID.ascending(), + sessionID: session.id, + messageID: a1, + type: "text", + text: "first reply", + }) + + const u2 = await addUser(session.id, "second") + const a2 = await addAssistant(session.id, u2, { finish: "end_turn" }) + await Session.updatePart({ + id: PartID.ascending(), + sessionID: session.id, + messageID: a2, + type: "text", + text: "second reply", + }) + + const c1 = await addUser(session.id) + await addCompactionPart(session.id, c1, u2) + const s1 = await addAssistant(session.id, c1, { summary: true, finish: "end_turn" }) + await Session.updatePart({ + id: PartID.ascending(), + sessionID: session.id, + messageID: s1, + type: "text", + text: "summary", + }) + + const u3 = await addUser(session.id, "third") + const a3 = await addAssistant(session.id, u3, { finish: "end_turn" }) + await Session.updatePart({ + id: PartID.ascending(), + sessionID: session.id, + messageID: a3, + type: "text", + text: "third reply", + }) + + const result = MessageV2.filterCompacted(MessageV2.stream(session.id)) + + expect(result.map((item) => item.info.id)).toEqual([u2, a2, c1, s1, u3, a3]) + + await Session.remove(session.id) + }, + }) + }) + + test("prefers latest compaction boundary when repeated compactions exist", async () => { + await Instance.provide({ + directory: root, + fn: async () => { + const session = await Session.create({}) + + const u1 = await addUser(session.id, "first") + const a1 = await addAssistant(session.id, u1, { finish: "end_turn" }) + await Session.updatePart({ + id: PartID.ascending(), + sessionID: session.id, + messageID: a1, + type: "text", + text: "first reply", + }) + + const u2 = await addUser(session.id, "second") + const a2 = await addAssistant(session.id, u2, { finish: "end_turn" }) + await Session.updatePart({ + id: PartID.ascending(), + sessionID: session.id, + messageID: a2, + type: "text", + text: "second reply", + }) + + const c1 = await addUser(session.id) + await addCompactionPart(session.id, c1, u2) + const s1 = await addAssistant(session.id, c1, { summary: true, finish: "end_turn" }) + await Session.updatePart({ + id: PartID.ascending(), + sessionID: session.id, + messageID: s1, + type: "text", + text: "summary one", + }) + + const u3 = await addUser(session.id, "third") + const a3 = await addAssistant(session.id, u3, { finish: "end_turn" }) + await Session.updatePart({ + id: PartID.ascending(), + sessionID: session.id, + messageID: a3, + type: "text", + text: "third reply", + }) + + const c2 = await addUser(session.id) + await addCompactionPart(session.id, c2, u3) + const s2 = await addAssistant(session.id, c2, { summary: true, finish: "end_turn" }) + await Session.updatePart({ + id: PartID.ascending(), + sessionID: session.id, + messageID: s2, + type: "text", + text: "summary two", + }) + + const u4 = await addUser(session.id, "fourth") + const a4 = await addAssistant(session.id, u4, { finish: "end_turn" }) + await Session.updatePart({ + id: PartID.ascending(), + sessionID: session.id, + messageID: a4, + type: "text", + text: "fourth reply", + }) + + const result = MessageV2.filterCompacted(MessageV2.stream(session.id)) + + expect(result.map((item) => item.info.id)).toEqual([u3, a3, c2, s2, u4, a4]) + + await Session.remove(session.id) + }, + }) + }) + test("works with array input", () => { // filterCompacted accepts any Iterable, not just generators const id = MessageID.ascending() From aa86fb75adb61cec725d3b2b26b574eb701008d7 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Fri, 10 Apr 2026 09:36:39 +0530 Subject: [PATCH 002/335] refactor compaction tail selection --- packages/opencode/src/session/compaction.ts | 75 ++++++++++++++------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 69ecc44ba2..9ca98804d0 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -39,6 +39,11 @@ export namespace SessionCompaction { const DEFAULT_TAIL_TURNS = 2 const MIN_TAIL_TOKENS = 2_000 const MAX_TAIL_TOKENS = 8_000 + type Turn = { + start: number + end: number + id: MessageID + } function usable(input: { cfg: Config.Info; model: Provider.Model }) { const reserved = @@ -55,6 +60,24 @@ export namespace SessionCompaction { ) } + function turns(messages: MessageV2.WithParts[]) { + const result: Turn[] = [] + for (let i = 0; i < messages.length; i++) { + const msg = messages[i] + if (msg.info.role !== "user") continue + if (msg.parts.some((part) => part.type === "compaction")) continue + result.push({ + start: i, + end: messages.length, + id: msg.info.id, + }) + } + for (let i = 0; i < result.length - 1; i++) { + result[i].end = result[i + 1].start + } + return result + } + export interface Interface { readonly isOverflow: (input: { tokens: MessageV2.Assistant["tokens"] @@ -123,36 +146,36 @@ export namespace SessionCompaction { const limit = input.cfg.compaction?.tail_turns ?? DEFAULT_TAIL_TURNS if (limit <= 0) return { head: input.messages, tail_start_id: undefined } const budget = tailBudget({ cfg: input.cfg, model: input.model }) - const turns = input.messages.flatMap((msg, idx) => - msg.info.role === "user" && !msg.parts.some((part) => part.type === "compaction") ? [idx] : [], + const all = turns(input.messages) + if (!all.length) return { head: input.messages, tail_start_id: undefined } + const recent = all.slice(-limit) + const sizes = yield* Effect.forEach( + recent, + (turn) => + estimate({ + messages: input.messages.slice(turn.start, turn.end), + model: input.model, + }), + { concurrency: 1 }, ) - if (!turns.length) return { head: input.messages, tail_start_id: undefined } - - let total = 0 - let start = input.messages.length - let kept = 0 - - for (let i = turns.length - 1; i >= 0 && kept < limit; i--) { - const idx = turns[i] - const end = i + 1 < turns.length ? turns[i + 1] : input.messages.length - const size = yield* estimate({ - messages: input.messages.slice(idx, end), - model: input.model, - }) - if (kept === 0 && size > budget) { - log.info("tail fallback", { budget, size }) - return { head: input.messages, tail_start_id: undefined } - } - if (total + size > budget) break - total += size - start = idx - kept++ + if (sizes.at(-1)! > budget) { + log.info("tail fallback", { budget, size: sizes.at(-1) }) + return { head: input.messages, tail_start_id: undefined } } - if (kept === 0 || start === 0) return { head: input.messages, tail_start_id: undefined } + let total = 0 + let keep: Turn | undefined + for (let i = recent.length - 1; i >= 0; i--) { + const size = sizes[i] + if (total + size > budget) break + total += size + keep = recent[i] + } + + if (!keep || keep.start === 0) return { head: input.messages, tail_start_id: undefined } return { - head: input.messages.slice(0, start), - tail_start_id: input.messages[start]?.info.id, + head: input.messages.slice(0, keep.start), + tail_start_id: keep.id, } }) From 9819eb04614fd607cacb07d754052f1531a82331 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Thu, 9 Apr 2026 23:11:09 -0500 Subject: [PATCH 003/335] tweak: disable --- packages/opencode/src/session/compaction.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 9ca98804d0..6848ccf178 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -46,8 +46,7 @@ export namespace SessionCompaction { } function usable(input: { cfg: Config.Info; model: Provider.Model }) { - const reserved = - input.cfg.compaction?.reserved ?? Math.min(20_000, ProviderTransform.maxOutputTokens(input.model)) + const reserved = input.cfg.compaction?.reserved ?? Math.min(20_000, ProviderTransform.maxOutputTokens(input.model)) return input.model.limit.input ? Math.max(0, input.model.limit.input - reserved) : Math.max(0, input.model.limit.context - ProviderTransform.maxOutputTokens(input.model)) @@ -183,7 +182,7 @@ export namespace SessionCompaction { // calls, then erases output of older tool calls to free context space const prune = Effect.fn("SessionCompaction.prune")(function* (input: { sessionID: SessionID }) { const cfg = yield* config.get() - if (cfg.compaction?.prune === false) return + if (cfg.compaction?.prune !== true) return log.info("pruning") const msgs = yield* session From 42771c1db377d190b670ec623a951e2ad7d51c3d Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 16 Apr 2026 17:30:29 +0530 Subject: [PATCH 004/335] fix(compaction): budget retained tail with media --- packages/opencode/src/config/config.ts | 6 +- packages/opencode/src/session/compaction.ts | 22 ++----- packages/opencode/src/session/overflow.ts | 24 ++++--- .../opencode/test/session/compaction.test.ts | 62 +++++++++++++++++++ 4 files changed, 84 insertions(+), 30 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index b4e6268b14..97e96ccbf5 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1009,13 +1009,15 @@ export const Info = z .int() .min(0) .optional() - .describe("Number of recent real user turns to keep verbatim during compaction (default: 2)"), + .describe( + "Number of recent user turns, including their following assistant/tool responses, to keep verbatim during compaction (default: 2)", + ), tail_tokens: z .number() .int() .min(0) .optional() - .describe("Token budget for retained recent turns during compaction"), + .describe("Token budget for retained recent turn spans during compaction"), reserved: z .number() .int() diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 325eb5a3df..ff2b316c48 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -2,7 +2,7 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import * as Session from "./session" import { SessionID, MessageID, PartID } from "./schema" -import { Provider, ProviderTransform } from "../provider" +import { Provider } from "../provider" import { MessageV2 } from "./message-v2" import z from "zod" import { Token } from "../util" @@ -17,7 +17,7 @@ import { Effect, Layer, Context } from "effect" import { InstanceState } from "@/effect" import { makeRuntime } from "@/effect/run-service" import { fn } from "@/util/fn" -import { isOverflow as overflow } from "./overflow" +import { isOverflow as overflow, usable } from "./overflow" export namespace SessionCompaction { const log = Log.create({ service: "session.compaction" }) @@ -43,13 +43,6 @@ export namespace SessionCompaction { id: MessageID } - function usable(input: { cfg: Config.Info; model: Provider.Model }) { - const reserved = input.cfg.compaction?.reserved ?? Math.min(20_000, ProviderTransform.maxOutputTokens(input.model)) - return input.model.limit.input - ? Math.max(0, input.model.limit.input - reserved) - : Math.max(0, input.model.limit.context - ProviderTransform.maxOutputTokens(input.model)) - } - function tailBudget(input: { cfg: Config.Info; model: Provider.Model }) { return ( input.cfg.compaction?.tail_tokens ?? @@ -131,7 +124,7 @@ export namespace SessionCompaction { messages: MessageV2.WithParts[] model: Provider.Model }) { - const msgs = yield* MessageV2.toModelMessagesEffect(input.messages, input.model, { stripMedia: true }) + const msgs = yield* MessageV2.toModelMessagesEffect(input.messages, input.model) return Token.estimate(JSON.stringify(msgs)) }) @@ -282,14 +275,7 @@ export namespace SessionCompaction { { sessionID: input.sessionID }, { context: [], prompt: undefined }, ) - const defaultPrompt = `Summarize the older conversation history so another agent can continue the work with the retained recent turns. -The most recent conversation turns will remain verbatim outside this summary, so focus on older context that is still needed to understand and continue the work. -Include what we did, what we're doing, which files we're working on, and what we're going to do next. -The summary that you construct will be used so that another agent can read it and continue the work. -Do not call any tools. Respond only with the summary text. -Respond in the same language as the user's messages in the conversation. - -When constructing the summary, try to stick to this template: + const defaultPrompt = `When constructing the summary, try to stick to this template: --- ## Goal diff --git a/packages/opencode/src/session/overflow.ts b/packages/opencode/src/session/overflow.ts index 6f48a760df..477b5815b2 100644 --- a/packages/opencode/src/session/overflow.ts +++ b/packages/opencode/src/session/overflow.ts @@ -5,18 +5,22 @@ import type { MessageV2 } from "./message-v2" const COMPACTION_BUFFER = 20_000 -export function isOverflow(input: { cfg: Config.Info; tokens: MessageV2.Assistant["tokens"]; model: Provider.Model }) { - if (input.cfg.compaction?.auto === false) return false +export function usable(input: { cfg: Config.Info; model: Provider.Model }) { const context = input.model.limit.context - if (context === 0) return false - - const count = - input.tokens.total || input.tokens.input + input.tokens.output + input.tokens.cache.read + input.tokens.cache.write + if (context === 0) return 0 const reserved = input.cfg.compaction?.reserved ?? Math.min(COMPACTION_BUFFER, ProviderTransform.maxOutputTokens(input.model)) - const usable = input.model.limit.input - ? input.model.limit.input - reserved - : context - ProviderTransform.maxOutputTokens(input.model) - return count >= usable + return input.model.limit.input + ? Math.max(0, input.model.limit.input - reserved) + : Math.max(0, context - ProviderTransform.maxOutputTokens(input.model)) +} + +export function isOverflow(input: { cfg: Config.Info; tokens: MessageV2.Assistant["tokens"]; model: Provider.Model }) { + if (input.cfg.compaction?.auto === false) return false + if (input.model.limit.context === 0) return false + + const count = + input.tokens.total || input.tokens.input + input.tokens.output + input.tokens.cache.read + input.tokens.cache.write + return count >= usable(input) } diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 85bc6e54ad..015a1653a3 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -1044,6 +1044,68 @@ describe("session.compaction.process", () => { }) }) + test("falls back to full summary when retained tail media exceeds tail budget", async () => { + await using tmp = await tmpdir({ git: true }) + const stub = llm() + let captured = "" + stub.push( + reply("summary", (input) => { + captured = JSON.stringify(input.messages) + }), + ) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await svc.create({}) + await user(session.id, "older") + const recent = await user(session.id, "recent image turn") + await svc.updatePart({ + id: PartID.ascending(), + messageID: recent.id, + sessionID: session.id, + type: "file", + mime: "image/png", + filename: "big.png", + url: `data:image/png;base64,${"a".repeat(4_000)}`, + }) + await SessionCompaction.create({ + sessionID: session.id, + agent: "build", + model: ref, + auto: false, + }) + + const rt = liveRuntime(stub.layer, wide(), cfg({ tail_turns: 1, tail_tokens: 100 })) + try { + const msgs = await svc.messages({ sessionID: session.id }) + const parent = msgs.at(-1)?.info.id + expect(parent).toBeTruthy() + await rt.runPromise( + SessionCompaction.Service.use((svc) => + svc.process({ + parentID: parent!, + messages: msgs, + sessionID: session.id, + auto: false, + }), + ), + ) + + const part = (await svc.messages({ sessionID: session.id })) + .at(-2) + ?.parts.find((item) => item.type === "compaction") + + expect(part?.type).toBe("compaction") + if (part?.type === "compaction") expect(part.tail_start_id).toBeUndefined() + expect(captured).toContain("recent image turn") + expect(captured).toContain("Attached image/png: big.png") + } finally { + await rt.dispose() + } + }, + }) + }) + test("allows plugins to disable synthetic continue prompt", async () => { await using tmp = await tmpdir() await Instance.provide({ From cb18f2ef407c49e7e91e03f0b7c4a72c2d4d05c1 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:45:35 -0500 Subject: [PATCH 005/335] fix: ensure azure sets prompt cache key by default (#22957) --- packages/opencode/src/provider/transform.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 492db40520..0ebd8bbf59 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -798,6 +798,7 @@ export function options(input: { if (input.model.api.npm === "@ai-sdk/azure") { result["store"] = true + result["promptCacheKey"] = input.sessionID } if (input.model.api.npm === "@openrouter/ai-sdk-provider") { From 23d48a7cf1af47870ef39def684eb8d569c66f4b Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 19:46:49 -0400 Subject: [PATCH 006/335] refactor: unwrap BusEvent namespace + self-reexport (#22962) --- packages/opencode/src/bus/bus-event.ts | 52 +++++++++++++------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/opencode/src/bus/bus-event.ts b/packages/opencode/src/bus/bus-event.ts index 369a40ed88..efaed94406 100644 --- a/packages/opencode/src/bus/bus-event.ts +++ b/packages/opencode/src/bus/bus-event.ts @@ -1,33 +1,33 @@ import z from "zod" import type { ZodType } from "zod" -export namespace BusEvent { - export type Definition = ReturnType +export type Definition = ReturnType - const registry = new Map() +const registry = new Map() - export function define(type: Type, properties: Properties) { - const result = { - type, - properties, - } - registry.set(type, result) - return result - } - - export function payloads() { - return registry - .entries() - .map(([type, def]) => { - return z - .object({ - type: z.literal(type), - properties: def.properties, - }) - .meta({ - ref: `Event.${def.type}`, - }) - }) - .toArray() +export function define(type: Type, properties: Properties) { + const result = { + type, + properties, } + registry.set(type, result) + return result } + +export function payloads() { + return registry + .entries() + .map(([type, def]) => { + return z + .object({ + type: z.literal(type), + properties: def.properties, + }) + .meta({ + ref: `Event.${def.type}`, + }) + }) + .toArray() +} + +export * as BusEvent from "./bus-event" From e2d161dfdd54fdd30f8e36e8cf4f46e261dab96e Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 19:48:24 -0400 Subject: [PATCH 007/335] refactor: unwrap Identifier namespace + self-reexport (#22963) --- packages/opencode/src/id/id.ts | 162 ++++++++++++++++----------------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/packages/opencode/src/id/id.ts b/packages/opencode/src/id/id.ts index 3d4cddf530..46c210fa5d 100644 --- a/packages/opencode/src/id/id.ts +++ b/packages/opencode/src/id/id.ts @@ -1,86 +1,86 @@ import z from "zod" import { randomBytes } from "crypto" -export namespace Identifier { - const prefixes = { - event: "evt", - session: "ses", - message: "msg", - permission: "per", - question: "que", - user: "usr", - part: "prt", - pty: "pty", - tool: "tool", - workspace: "wrk", - entry: "ent", - } as const +const prefixes = { + event: "evt", + session: "ses", + message: "msg", + permission: "per", + question: "que", + user: "usr", + part: "prt", + pty: "pty", + tool: "tool", + workspace: "wrk", + entry: "ent", +} as const - export function schema(prefix: keyof typeof prefixes) { - return z.string().startsWith(prefixes[prefix]) - } - - const LENGTH = 26 - - // State for monotonic ID generation - let lastTimestamp = 0 - let counter = 0 - - export function ascending(prefix: keyof typeof prefixes, given?: string) { - return generateID(prefix, "ascending", given) - } - - export function descending(prefix: keyof typeof prefixes, given?: string) { - return generateID(prefix, "descending", given) - } - - function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string { - if (!given) { - return create(prefixes[prefix], direction) - } - - if (!given.startsWith(prefixes[prefix])) { - throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`) - } - return given - } - - function randomBase62(length: number): string { - const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - let result = "" - const bytes = randomBytes(length) - for (let i = 0; i < length; i++) { - result += chars[bytes[i] % 62] - } - return result - } - - export function create(prefix: string, direction: "descending" | "ascending", timestamp?: number): string { - const currentTimestamp = timestamp ?? Date.now() - - if (currentTimestamp !== lastTimestamp) { - lastTimestamp = currentTimestamp - counter = 0 - } - counter++ - - let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter) - - now = direction === "descending" ? ~now : now - - const timeBytes = Buffer.alloc(6) - for (let i = 0; i < 6; i++) { - timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff)) - } - - return prefix + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12) - } - - /** Extract timestamp from an ascending ID. Does not work with descending IDs. */ - export function timestamp(id: string): number { - const prefix = id.split("_")[0] - const hex = id.slice(prefix.length + 1, prefix.length + 13) - const encoded = BigInt("0x" + hex) - return Number(encoded / BigInt(0x1000)) - } +export function schema(prefix: keyof typeof prefixes) { + return z.string().startsWith(prefixes[prefix]) } + +const LENGTH = 26 + +// State for monotonic ID generation +let lastTimestamp = 0 +let counter = 0 + +export function ascending(prefix: keyof typeof prefixes, given?: string) { + return generateID(prefix, "ascending", given) +} + +export function descending(prefix: keyof typeof prefixes, given?: string) { + return generateID(prefix, "descending", given) +} + +function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string { + if (!given) { + return create(prefixes[prefix], direction) + } + + if (!given.startsWith(prefixes[prefix])) { + throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`) + } + return given +} + +function randomBase62(length: number): string { + const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + let result = "" + const bytes = randomBytes(length) + for (let i = 0; i < length; i++) { + result += chars[bytes[i] % 62] + } + return result +} + +export function create(prefix: string, direction: "descending" | "ascending", timestamp?: number): string { + const currentTimestamp = timestamp ?? Date.now() + + if (currentTimestamp !== lastTimestamp) { + lastTimestamp = currentTimestamp + counter = 0 + } + counter++ + + let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter) + + now = direction === "descending" ? ~now : now + + const timeBytes = Buffer.alloc(6) + for (let i = 0; i < 6; i++) { + timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff)) + } + + return prefix + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12) +} + +/** Extract timestamp from an ascending ID. Does not work with descending IDs. */ +export function timestamp(id: string): number { + const prefix = id.split("_")[0] + const hex = id.slice(prefix.length + 1, prefix.length + 13) + const encoded = BigInt("0x" + hex) + return Number(encoded / BigInt(0x1000)) +} + +export * as Identifier from "./id" From 30fc791480ebdabc9c62c70713e6cb52b44caff2 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 19:49:52 -0400 Subject: [PATCH 008/335] refactor: unwrap Ripgrep namespace + self-reexport (#22965) --- packages/opencode/src/file/ripgrep.ts | 1048 ++++++++++++------------- 1 file changed, 524 insertions(+), 524 deletions(-) diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index 9a78c5b7fb..ac450108e1 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -8,568 +8,568 @@ import { ripgrep } from "ripgrep" import { Filesystem } from "@/util" import { Log } from "@/util" -export namespace Ripgrep { - const log = Log.create({ service: "ripgrep" }) +const log = Log.create({ service: "ripgrep" }) - const Stats = z.object({ - elapsed: z.object({ - secs: z.number(), - nanos: z.number(), +const Stats = z.object({ + elapsed: z.object({ + secs: z.number(), + nanos: z.number(), + human: z.string(), + }), + searches: z.number(), + searches_with_match: z.number(), + bytes_searched: z.number(), + bytes_printed: z.number(), + matched_lines: z.number(), + matches: z.number(), +}) + +const Begin = z.object({ + type: z.literal("begin"), + data: z.object({ + path: z.object({ + text: z.string(), + }), + }), +}) + +export const Match = z.object({ + type: z.literal("match"), + data: z.object({ + path: z.object({ + text: z.string(), + }), + lines: z.object({ + text: z.string(), + }), + line_number: z.number(), + absolute_offset: z.number(), + submatches: z.array( + z.object({ + match: z.object({ + text: z.string(), + }), + start: z.number(), + end: z.number(), + }), + ), + }), +}) + +const End = z.object({ + type: z.literal("end"), + data: z.object({ + path: z.object({ + text: z.string(), + }), + binary_offset: z.number().nullable(), + stats: Stats, + }), +}) + +const Summary = z.object({ + type: z.literal("summary"), + data: z.object({ + elapsed_total: z.object({ human: z.string(), + nanos: z.number(), + secs: z.number(), }), - searches: z.number(), - searches_with_match: z.number(), - bytes_searched: z.number(), - bytes_printed: z.number(), - matched_lines: z.number(), - matches: z.number(), - }) + stats: Stats, + }), +}) - const Begin = z.object({ - type: z.literal("begin"), - data: z.object({ - path: z.object({ - text: z.string(), - }), - }), - }) +const Result = z.union([Begin, Match, End, Summary]) - export const Match = z.object({ - type: z.literal("match"), - data: z.object({ - path: z.object({ - text: z.string(), - }), - lines: z.object({ - text: z.string(), - }), - line_number: z.number(), - absolute_offset: z.number(), - submatches: z.array( - z.object({ - match: z.object({ - text: z.string(), - }), - start: z.number(), - end: z.number(), - }), - ), - }), - }) +export type Result = z.infer +export type Match = z.infer +export type Item = Match["data"] +export type Begin = z.infer +export type End = z.infer +export type Summary = z.infer +export type Row = Match["data"] - const End = z.object({ - type: z.literal("end"), - data: z.object({ - path: z.object({ - text: z.string(), - }), - binary_offset: z.number().nullable(), - stats: Stats, - }), - }) +export interface SearchResult { + items: Item[] + partial: boolean +} - const Summary = z.object({ - type: z.literal("summary"), - data: z.object({ - elapsed_total: z.object({ - human: z.string(), - nanos: z.number(), - secs: z.number(), - }), - stats: Stats, - }), - }) +export interface FilesInput { + cwd: string + glob?: string[] + hidden?: boolean + follow?: boolean + maxDepth?: number + signal?: AbortSignal +} - const Result = z.union([Begin, Match, End, Summary]) +export interface SearchInput { + cwd: string + pattern: string + glob?: string[] + limit?: number + follow?: boolean + file?: string[] + signal?: AbortSignal +} - export type Result = z.infer - export type Match = z.infer - export type Item = Match["data"] - export type Begin = z.infer - export type End = z.infer - export type Summary = z.infer - export type Row = Match["data"] +export interface TreeInput { + cwd: string + limit?: number + signal?: AbortSignal +} - export interface SearchResult { - items: Item[] - partial: boolean +export interface Interface { + readonly files: (input: FilesInput) => Stream.Stream + readonly tree: (input: TreeInput) => Effect.Effect + readonly search: (input: SearchInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/Ripgrep") {} + +type Run = { kind: "files" | "search"; cwd: string; args: string[] } + +type WorkerResult = { + type: "result" + code: number + stdout: string + stderr: string +} + +type WorkerLine = { + type: "line" + line: string +} + +type WorkerDone = { + type: "done" + code: number + stderr: string +} + +type WorkerError = { + type: "error" + error: { + message: string + name?: string + stack?: string } +} - export interface FilesInput { - cwd: string - glob?: string[] - hidden?: boolean - follow?: boolean - maxDepth?: number - signal?: AbortSignal +function env() { + const env = Object.fromEntries( + Object.entries(process.env).filter((item): item is [string, string] => item[1] !== undefined), + ) + delete env.RIPGREP_CONFIG_PATH + return env +} + +function text(input: unknown) { + if (typeof input === "string") return input + if (input instanceof ArrayBuffer) return Buffer.from(input).toString() + if (ArrayBuffer.isView(input)) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString() + return String(input) +} + +function toError(input: unknown) { + if (input instanceof Error) return input + if (typeof input === "string") return new Error(input) + return new Error(String(input)) +} + +function abort(signal?: AbortSignal) { + const err = signal?.reason + if (err instanceof Error) return err + const out = new Error("Aborted") + out.name = "AbortError" + return out +} + +function error(stderr: string, code: number) { + const err = new Error(stderr.trim() || `ripgrep failed with code ${code}`) + err.name = "RipgrepError" + return err +} + +function clean(file: string) { + return path.normalize(file.replace(/^\.[\\/]/, "")) +} + +function row(data: Row): Row { + return { + ...data, + path: { + ...data.path, + text: clean(data.path.text), + }, } +} - export interface SearchInput { - cwd: string - pattern: string - glob?: string[] - limit?: number - follow?: boolean - file?: string[] - signal?: AbortSignal +function opts(cwd: string) { + return { + env: env(), + preopens: { ".": cwd }, } +} - export interface TreeInput { - cwd: string - limit?: number - signal?: AbortSignal - } - - export interface Interface { - readonly files: (input: FilesInput) => Stream.Stream - readonly tree: (input: TreeInput) => Effect.Effect - readonly search: (input: SearchInput) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/Ripgrep") {} - - type Run = { kind: "files" | "search"; cwd: string; args: string[] } - - type WorkerResult = { - type: "result" - code: number - stdout: string - stderr: string - } - - type WorkerLine = { - type: "line" - line: string - } - - type WorkerDone = { - type: "done" - code: number - stderr: string - } - - type WorkerError = { - type: "error" - error: { - message: string - name?: string - stack?: string - } - } - - function env() { - const env = Object.fromEntries( - Object.entries(process.env).filter((item): item is [string, string] => item[1] !== undefined), - ) - delete env.RIPGREP_CONFIG_PATH - return env - } - - function text(input: unknown) { - if (typeof input === "string") return input - if (input instanceof ArrayBuffer) return Buffer.from(input).toString() - if (ArrayBuffer.isView(input)) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString() - return String(input) - } - - function toError(input: unknown) { - if (input instanceof Error) return input - if (typeof input === "string") return new Error(input) - return new Error(String(input)) - } - - function abort(signal?: AbortSignal) { - const err = signal?.reason - if (err instanceof Error) return err - const out = new Error("Aborted") - out.name = "AbortError" - return out - } - - function error(stderr: string, code: number) { - const err = new Error(stderr.trim() || `ripgrep failed with code ${code}`) - err.name = "RipgrepError" - return err - } - - function clean(file: string) { - return path.normalize(file.replace(/^\.[\\/]/, "")) - } - - function row(data: Row): Row { - return { - ...data, - path: { - ...data.path, - text: clean(data.path.text), - }, - } - } - - function opts(cwd: string) { - return { - env: env(), - preopens: { ".": cwd }, - } - } - - function check(cwd: string) { - return Effect.tryPromise({ - try: () => fs.stat(cwd).catch(() => undefined), - catch: toError, - }).pipe( - Effect.flatMap((stat) => - stat?.isDirectory() - ? Effect.void - : Effect.fail( - Object.assign(new Error(`No such file or directory: '${cwd}'`), { - code: "ENOENT", - errno: -2, - path: cwd, - }), - ), - ), - ) - } - - function filesArgs(input: FilesInput) { - const args = ["--files", "--glob=!.git/*"] - if (input.follow) args.push("--follow") - if (input.hidden !== false) args.push("--hidden") - if (input.maxDepth !== undefined) args.push(`--max-depth=${input.maxDepth}`) - if (input.glob) { - for (const glob of input.glob) { - args.push(`--glob=${glob}`) - } - } - args.push(".") - return args - } - - function searchArgs(input: SearchInput) { - const args = ["--json", "--hidden", "--glob=!.git/*", "--no-messages"] - if (input.follow) args.push("--follow") - if (input.glob) { - for (const glob of input.glob) { - args.push(`--glob=${glob}`) - } - } - if (input.limit) args.push(`--max-count=${input.limit}`) - args.push("--", input.pattern, ...(input.file ?? ["."])) - return args - } - - function parse(stdout: string) { - return stdout - .trim() - .split(/\r?\n/) - .filter(Boolean) - .map((line) => Result.parse(JSON.parse(line))) - .flatMap((item) => (item.type === "match" ? [row(item.data)] : [])) - } - - declare const OPENCODE_RIPGREP_WORKER_PATH: string - - function target(): Effect.Effect { - if (typeof OPENCODE_RIPGREP_WORKER_PATH !== "undefined") { - return Effect.succeed(OPENCODE_RIPGREP_WORKER_PATH) - } - const js = new URL("./ripgrep.worker.js", import.meta.url) - return Effect.tryPromise({ - try: () => Filesystem.exists(fileURLToPath(js)), - catch: toError, - }).pipe(Effect.map((exists) => (exists ? js : new URL("./ripgrep.worker.ts", import.meta.url)))) - } - - function worker() { - return target().pipe(Effect.flatMap((file) => Effect.sync(() => new Worker(file, { env: env() })))) - } - - function drain(buf: string, chunk: unknown, push: (line: string) => void) { - const lines = (buf + text(chunk)).split(/\r?\n/) - buf = lines.pop() || "" - for (const line of lines) { - if (line) push(line) - } - return buf - } - - function fail(queue: Queue.Queue, err: Error) { - Queue.failCauseUnsafe(queue, Cause.fail(err)) - } - - function searchDirect(input: SearchInput) { - return Effect.tryPromise({ - try: () => - ripgrep(searchArgs(input), { - buffer: true, - ...opts(input.cwd), - }), - catch: toError, - }).pipe( - Effect.flatMap((ret) => { - const out = ret.stdout ?? "" - if (ret.code !== 0 && ret.code !== 1 && ret.code !== 2) { - return Effect.fail(error(ret.stderr ?? "", ret.code ?? 1)) - } - return Effect.sync(() => ({ - items: ret.code === 1 ? [] : parse(out), - partial: ret.code === 2, - })) - }), - ) - } - - function searchWorker(input: SearchInput) { - if (input.signal?.aborted) return Effect.fail(abort(input.signal)) - - return Effect.acquireUseRelease( - worker(), - (w) => - Effect.callback((resume, signal) => { - let open = true - const done = (effect: Effect.Effect) => { - if (!open) return - open = false - resume(effect) - } - const onabort = () => done(Effect.fail(abort(input.signal))) - - w.onerror = (evt) => { - done(Effect.fail(toError(evt.error ?? evt.message))) - } - w.onmessage = (evt: MessageEvent) => { - const msg = evt.data - if (msg.type === "error") { - done(Effect.fail(Object.assign(new Error(msg.error.message), msg.error))) - return - } - if (msg.code === 1) { - done(Effect.succeed({ items: [], partial: false })) - return - } - if (msg.code !== 0 && msg.code !== 1 && msg.code !== 2) { - done(Effect.fail(error(msg.stderr, msg.code))) - return - } - done( - Effect.sync(() => ({ - items: parse(msg.stdout), - partial: msg.code === 2, - })), - ) - } - - input.signal?.addEventListener("abort", onabort, { once: true }) - signal.addEventListener("abort", onabort, { once: true }) - w.postMessage({ - kind: "search", - cwd: input.cwd, - args: searchArgs(input), - } satisfies Run) - - return Effect.sync(() => { - input.signal?.removeEventListener("abort", onabort) - signal.removeEventListener("abort", onabort) - w.onerror = null - w.onmessage = null - }) - }), - (w) => Effect.sync(() => w.terminate()), - ) - } - - function filesDirect(input: FilesInput) { - return Stream.callback( - Effect.fnUntraced(function* (queue: Queue.Queue) { - let buf = "" - let err = "" - - const out = { - write(chunk: unknown) { - buf = drain(buf, chunk, (line) => { - Queue.offerUnsafe(queue, clean(line)) - }) - }, - } - - const stderr = { - write(chunk: unknown) { - err += text(chunk) - }, - } - - yield* Effect.forkScoped( - Effect.gen(function* () { - yield* check(input.cwd) - const ret = yield* Effect.tryPromise({ - try: () => - ripgrep(filesArgs(input), { - stdout: out, - stderr, - ...opts(input.cwd), - }), - catch: toError, - }) - if (buf) Queue.offerUnsafe(queue, clean(buf)) - if (ret.code === 0 || ret.code === 1) { - Queue.endUnsafe(queue) - return - } - fail(queue, error(err, ret.code ?? 1)) - }).pipe( - Effect.catch((err) => - Effect.sync(() => { - fail(queue, err) - }), - ), +function check(cwd: string) { + return Effect.tryPromise({ + try: () => fs.stat(cwd).catch(() => undefined), + catch: toError, + }).pipe( + Effect.flatMap((stat) => + stat?.isDirectory() + ? Effect.void + : Effect.fail( + Object.assign(new Error(`No such file or directory: '${cwd}'`), { + code: "ENOENT", + errno: -2, + path: cwd, + }), ), - ) - }), - ) + ), + ) +} + +function filesArgs(input: FilesInput) { + const args = ["--files", "--glob=!.git/*"] + if (input.follow) args.push("--follow") + if (input.hidden !== false) args.push("--hidden") + if (input.maxDepth !== undefined) args.push(`--max-depth=${input.maxDepth}`) + if (input.glob) { + for (const glob of input.glob) { + args.push(`--glob=${glob}`) + } } + args.push(".") + return args +} - function filesWorker(input: FilesInput) { - return Stream.callback( - Effect.fnUntraced(function* (queue: Queue.Queue) { - if (input.signal?.aborted) { - fail(queue, abort(input.signal)) - return - } +function searchArgs(input: SearchInput) { + const args = ["--json", "--hidden", "--glob=!.git/*", "--no-messages"] + if (input.follow) args.push("--follow") + if (input.glob) { + for (const glob of input.glob) { + args.push(`--glob=${glob}`) + } + } + if (input.limit) args.push(`--max-count=${input.limit}`) + args.push("--", input.pattern, ...(input.file ?? ["."])) + return args +} - const w = yield* Effect.acquireRelease(worker(), (w) => Effect.sync(() => w.terminate())) +function parse(stdout: string) { + return stdout + .trim() + .split(/\r?\n/) + .filter(Boolean) + .map((line) => Result.parse(JSON.parse(line))) + .flatMap((item) => (item.type === "match" ? [row(item.data)] : [])) +} + +declare const OPENCODE_RIPGREP_WORKER_PATH: string + +function target(): Effect.Effect { + if (typeof OPENCODE_RIPGREP_WORKER_PATH !== "undefined") { + return Effect.succeed(OPENCODE_RIPGREP_WORKER_PATH) + } + const js = new URL("./ripgrep.worker.js", import.meta.url) + return Effect.tryPromise({ + try: () => Filesystem.exists(fileURLToPath(js)), + catch: toError, + }).pipe(Effect.map((exists) => (exists ? js : new URL("./ripgrep.worker.ts", import.meta.url)))) +} + +function worker() { + return target().pipe(Effect.flatMap((file) => Effect.sync(() => new Worker(file, { env: env() })))) +} + +function drain(buf: string, chunk: unknown, push: (line: string) => void) { + const lines = (buf + text(chunk)).split(/\r?\n/) + buf = lines.pop() || "" + for (const line of lines) { + if (line) push(line) + } + return buf +} + +function fail(queue: Queue.Queue, err: Error) { + Queue.failCauseUnsafe(queue, Cause.fail(err)) +} + +function searchDirect(input: SearchInput) { + return Effect.tryPromise({ + try: () => + ripgrep(searchArgs(input), { + buffer: true, + ...opts(input.cwd), + }), + catch: toError, + }).pipe( + Effect.flatMap((ret) => { + const out = ret.stdout ?? "" + if (ret.code !== 0 && ret.code !== 1 && ret.code !== 2) { + return Effect.fail(error(ret.stderr ?? "", ret.code ?? 1)) + } + return Effect.sync(() => ({ + items: ret.code === 1 ? [] : parse(out), + partial: ret.code === 2, + })) + }), + ) +} + +function searchWorker(input: SearchInput) { + if (input.signal?.aborted) return Effect.fail(abort(input.signal)) + + return Effect.acquireUseRelease( + worker(), + (w) => + Effect.callback((resume, signal) => { let open = true - const close = () => { - if (!open) return false + const done = (effect: Effect.Effect) => { + if (!open) return open = false - return true - } - const onabort = () => { - if (!close()) return - fail(queue, abort(input.signal)) + resume(effect) } + const onabort = () => done(Effect.fail(abort(input.signal))) w.onerror = (evt) => { - if (!close()) return - fail(queue, toError(evt.error ?? evt.message)) + done(Effect.fail(toError(evt.error ?? evt.message))) } - w.onmessage = (evt: MessageEvent) => { + w.onmessage = (evt: MessageEvent) => { const msg = evt.data - if (msg.type === "line") { - if (open) Queue.offerUnsafe(queue, msg.line) - return - } - if (!close()) return if (msg.type === "error") { - fail(queue, Object.assign(new Error(msg.error.message), msg.error)) + done(Effect.fail(Object.assign(new Error(msg.error.message), msg.error))) return } - if (msg.code === 0 || msg.code === 1) { - Queue.endUnsafe(queue) + if (msg.code === 1) { + done(Effect.succeed({ items: [], partial: false })) return } - fail(queue, error(msg.stderr, msg.code)) - } - - yield* Effect.acquireRelease( - Effect.sync(() => { - input.signal?.addEventListener("abort", onabort, { once: true }) - w.postMessage({ - kind: "files", - cwd: input.cwd, - args: filesArgs(input), - } satisfies Run) - }), - () => - Effect.sync(() => { - input.signal?.removeEventListener("abort", onabort) - w.onerror = null - w.onmessage = null - }), - ) - }), - ) - } - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const source = (input: FilesInput) => { - const useWorker = !!input.signal && typeof Worker !== "undefined" - if (!useWorker && input.signal) { - log.warn("worker unavailable, ripgrep abort disabled") - } - return useWorker ? filesWorker(input) : filesDirect(input) - } - - const files: Interface["files"] = (input) => source(input) - - const tree: Interface["tree"] = Effect.fn("Ripgrep.tree")(function* (input: TreeInput) { - log.info("tree", input) - const list = Array.from(yield* source({ cwd: input.cwd, signal: input.signal }).pipe(Stream.runCollect)) - - interface Node { - name: string - children: Map - } - - function child(node: Node, name: string) { - const item = node.children.get(name) - if (item) return item - const next = { name, children: new Map() } - node.children.set(name, next) - return next - } - - function count(node: Node): number { - return Array.from(node.children.values()).reduce((sum, child) => sum + 1 + count(child), 0) - } - - const root: Node = { name: "", children: new Map() } - for (const file of list) { - if (file.includes(".opencode")) continue - const parts = file.split(path.sep) - if (parts.length < 2) continue - let node = root - for (const part of parts.slice(0, -1)) { - node = child(node, part) + if (msg.code !== 0 && msg.code !== 1 && msg.code !== 2) { + done(Effect.fail(error(msg.stderr, msg.code))) + return } - } - - const total = count(root) - const limit = input.limit ?? total - const lines: string[] = [] - const queue: Array<{ node: Node; path: string }> = Array.from(root.children.values()) - .sort((a, b) => a.name.localeCompare(b.name)) - .map((node) => ({ node, path: node.name })) - - let used = 0 - for (let i = 0; i < queue.length && used < limit; i++) { - const item = queue[i] - lines.push(item.path) - used++ - queue.push( - ...Array.from(item.node.children.values()) - .sort((a, b) => a.name.localeCompare(b.name)) - .map((node) => ({ node, path: `${item.path}/${node.name}` })), + done( + Effect.sync(() => ({ + items: parse(msg.stdout), + partial: msg.code === 2, + })), ) } - if (total > used) lines.push(`[${total - used} truncated]`) - return lines.join("\n") - }) + input.signal?.addEventListener("abort", onabort, { once: true }) + signal.addEventListener("abort", onabort, { once: true }) + w.postMessage({ + kind: "search", + cwd: input.cwd, + args: searchArgs(input), + } satisfies Run) - const search: Interface["search"] = Effect.fn("Ripgrep.search")(function* (input: SearchInput) { - const useWorker = !!input.signal && typeof Worker !== "undefined" - if (!useWorker && input.signal) { - log.warn("worker unavailable, ripgrep abort disabled") - } - return yield* useWorker ? searchWorker(input) : searchDirect(input) - }) + return Effect.sync(() => { + input.signal?.removeEventListener("abort", onabort) + signal.removeEventListener("abort", onabort) + w.onerror = null + w.onmessage = null + }) + }), + (w) => Effect.sync(() => w.terminate()), + ) +} - return Service.of({ files, tree, search }) +function filesDirect(input: FilesInput) { + return Stream.callback( + Effect.fnUntraced(function* (queue: Queue.Queue) { + let buf = "" + let err = "" + + const out = { + write(chunk: unknown) { + buf = drain(buf, chunk, (line) => { + Queue.offerUnsafe(queue, clean(line)) + }) + }, + } + + const stderr = { + write(chunk: unknown) { + err += text(chunk) + }, + } + + yield* Effect.forkScoped( + Effect.gen(function* () { + yield* check(input.cwd) + const ret = yield* Effect.tryPromise({ + try: () => + ripgrep(filesArgs(input), { + stdout: out, + stderr, + ...opts(input.cwd), + }), + catch: toError, + }) + if (buf) Queue.offerUnsafe(queue, clean(buf)) + if (ret.code === 0 || ret.code === 1) { + Queue.endUnsafe(queue) + return + } + fail(queue, error(err, ret.code ?? 1)) + }).pipe( + Effect.catch((err) => + Effect.sync(() => { + fail(queue, err) + }), + ), + ), + ) }), ) - - export const defaultLayer = layer } + +function filesWorker(input: FilesInput) { + return Stream.callback( + Effect.fnUntraced(function* (queue: Queue.Queue) { + if (input.signal?.aborted) { + fail(queue, abort(input.signal)) + return + } + + const w = yield* Effect.acquireRelease(worker(), (w) => Effect.sync(() => w.terminate())) + let open = true + const close = () => { + if (!open) return false + open = false + return true + } + const onabort = () => { + if (!close()) return + fail(queue, abort(input.signal)) + } + + w.onerror = (evt) => { + if (!close()) return + fail(queue, toError(evt.error ?? evt.message)) + } + w.onmessage = (evt: MessageEvent) => { + const msg = evt.data + if (msg.type === "line") { + if (open) Queue.offerUnsafe(queue, msg.line) + return + } + if (!close()) return + if (msg.type === "error") { + fail(queue, Object.assign(new Error(msg.error.message), msg.error)) + return + } + if (msg.code === 0 || msg.code === 1) { + Queue.endUnsafe(queue) + return + } + fail(queue, error(msg.stderr, msg.code)) + } + + yield* Effect.acquireRelease( + Effect.sync(() => { + input.signal?.addEventListener("abort", onabort, { once: true }) + w.postMessage({ + kind: "files", + cwd: input.cwd, + args: filesArgs(input), + } satisfies Run) + }), + () => + Effect.sync(() => { + input.signal?.removeEventListener("abort", onabort) + w.onerror = null + w.onmessage = null + }), + ) + }), + ) +} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const source = (input: FilesInput) => { + const useWorker = !!input.signal && typeof Worker !== "undefined" + if (!useWorker && input.signal) { + log.warn("worker unavailable, ripgrep abort disabled") + } + return useWorker ? filesWorker(input) : filesDirect(input) + } + + const files: Interface["files"] = (input) => source(input) + + const tree: Interface["tree"] = Effect.fn("Ripgrep.tree")(function* (input: TreeInput) { + log.info("tree", input) + const list = Array.from(yield* source({ cwd: input.cwd, signal: input.signal }).pipe(Stream.runCollect)) + + interface Node { + name: string + children: Map + } + + function child(node: Node, name: string) { + const item = node.children.get(name) + if (item) return item + const next = { name, children: new Map() } + node.children.set(name, next) + return next + } + + function count(node: Node): number { + return Array.from(node.children.values()).reduce((sum, child) => sum + 1 + count(child), 0) + } + + const root: Node = { name: "", children: new Map() } + for (const file of list) { + if (file.includes(".opencode")) continue + const parts = file.split(path.sep) + if (parts.length < 2) continue + let node = root + for (const part of parts.slice(0, -1)) { + node = child(node, part) + } + } + + const total = count(root) + const limit = input.limit ?? total + const lines: string[] = [] + const queue: Array<{ node: Node; path: string }> = Array.from(root.children.values()) + .sort((a, b) => a.name.localeCompare(b.name)) + .map((node) => ({ node, path: node.name })) + + let used = 0 + for (let i = 0; i < queue.length && used < limit; i++) { + const item = queue[i] + lines.push(item.path) + used++ + queue.push( + ...Array.from(item.node.children.values()) + .sort((a, b) => a.name.localeCompare(b.name)) + .map((node) => ({ node, path: `${item.path}/${node.name}` })), + ) + } + + if (total > used) lines.push(`[${total - used} truncated]`) + return lines.join("\n") + }) + + const search: Interface["search"] = Effect.fn("Ripgrep.search")(function* (input: SearchInput) { + const useWorker = !!input.signal && typeof Worker !== "undefined" + if (!useWorker && input.signal) { + log.warn("worker unavailable, ripgrep abort disabled") + } + return yield* useWorker ? searchWorker(input) : searchDirect(input) + }) + + return Service.of({ files, tree, search }) + }), +) + +export const defaultLayer = layer + +export * as Ripgrep from "./ripgrep" From 218eca7c2bc95355f594c0fe50853326c86c469f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 19:50:11 -0400 Subject: [PATCH 009/335] refactor: unwrap MDNS namespace + self-reexport (#22968) --- packages/opencode/src/server/mdns.ts | 88 ++++++++++++++-------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/packages/opencode/src/server/mdns.ts b/packages/opencode/src/server/mdns.ts index 2011771a20..580456754d 100644 --- a/packages/opencode/src/server/mdns.ts +++ b/packages/opencode/src/server/mdns.ts @@ -3,58 +3,58 @@ import { Bonjour } from "bonjour-service" const log = Log.create({ service: "mdns" }) -export namespace MDNS { - let bonjour: Bonjour | undefined - let currentPort: number | undefined +let bonjour: Bonjour | undefined +let currentPort: number | undefined - export function publish(port: number, domain?: string) { - if (currentPort === port) return - if (bonjour) unpublish() +export function publish(port: number, domain?: string) { + if (currentPort === port) return + if (bonjour) unpublish() - try { - const host = domain ?? "opencode.local" - const name = `opencode-${port}` - bonjour = new Bonjour() - const service = bonjour.publish({ - name, - type: "http", - host, - port, - txt: { path: "/" }, - }) + try { + const host = domain ?? "opencode.local" + const name = `opencode-${port}` + bonjour = new Bonjour() + const service = bonjour.publish({ + name, + type: "http", + host, + port, + txt: { path: "/" }, + }) - service.on("up", () => { - log.info("mDNS service published", { name, port }) - }) + service.on("up", () => { + log.info("mDNS service published", { name, port }) + }) - service.on("error", (err) => { - log.error("mDNS service error", { error: err }) - }) + service.on("error", (err) => { + log.error("mDNS service error", { error: err }) + }) - currentPort = port - } catch (err) { - log.error("mDNS publish failed", { error: err }) - if (bonjour) { - try { - bonjour.destroy() - } catch {} - } - bonjour = undefined - currentPort = undefined - } - } - - export function unpublish() { + currentPort = port + } catch (err) { + log.error("mDNS publish failed", { error: err }) if (bonjour) { try { - bonjour.unpublishAll() bonjour.destroy() - } catch (err) { - log.error("mDNS unpublish failed", { error: err }) - } - bonjour = undefined - currentPort = undefined - log.info("mDNS service unpublished") + } catch {} } + bonjour = undefined + currentPort = undefined } } + +export function unpublish() { + if (bonjour) { + try { + bonjour.unpublishAll() + bonjour.destroy() + } catch (err) { + log.error("mDNS unpublish failed", { error: err }) + } + bonjour = undefined + currentPort = undefined + log.info("mDNS service unpublished") + } +} + +export * as MDNS from "./mdns" From 715786bbf96304f617c5f2a48ed49fe6101c90ef Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 19:50:15 -0400 Subject: [PATCH 010/335] refactor: unwrap FileTime namespace + self-reexport (#22966) --- packages/opencode/src/file/time.ts | 208 ++++++++++++++--------------- 1 file changed, 104 insertions(+), 104 deletions(-) diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index 327eadbef5..cc26682d57 100644 --- a/packages/opencode/src/file/time.ts +++ b/packages/opencode/src/file/time.ts @@ -5,109 +5,109 @@ import { Flag } from "@/flag/flag" import type { SessionID } from "@/session/schema" import { Log } from "../util" -export namespace FileTime { - const log = Log.create({ service: "file.time" }) +const log = Log.create({ service: "file.time" }) - export type Stamp = { - readonly read: Date - readonly mtime: number | undefined - readonly size: number | undefined - } - - const session = (reads: Map>, sessionID: SessionID) => { - const value = reads.get(sessionID) - if (value) return value - - const next = new Map() - reads.set(sessionID, next) - return next - } - - interface State { - reads: Map> - locks: Map - } - - export interface Interface { - readonly read: (sessionID: SessionID, file: string) => Effect.Effect - readonly get: (sessionID: SessionID, file: string) => Effect.Effect - readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect - readonly withLock: (filepath: string, fn: () => Effect.Effect) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/FileTime") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const fsys = yield* AppFileSystem.Service - const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK - - const stamp = Effect.fnUntraced(function* (file: string) { - const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.void)) - return { - read: yield* DateTime.nowAsDate, - mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined, - size: info ? Number(info.size) : undefined, - } - }) - const state = yield* InstanceState.make( - Effect.fn("FileTime.state")(() => - Effect.succeed({ - reads: new Map>(), - locks: new Map(), - }), - ), - ) - - const getLock = Effect.fn("FileTime.lock")(function* (filepath: string) { - filepath = AppFileSystem.normalizePath(filepath) - const locks = (yield* InstanceState.get(state)).locks - const lock = locks.get(filepath) - if (lock) return lock - - const next = Semaphore.makeUnsafe(1) - locks.set(filepath, next) - return next - }) - - const read = Effect.fn("FileTime.read")(function* (sessionID: SessionID, file: string) { - file = AppFileSystem.normalizePath(file) - const reads = (yield* InstanceState.get(state)).reads - log.info("read", { sessionID, file }) - session(reads, sessionID).set(file, yield* stamp(file)) - }) - - const get = Effect.fn("FileTime.get")(function* (sessionID: SessionID, file: string) { - file = AppFileSystem.normalizePath(file) - const reads = (yield* InstanceState.get(state)).reads - return reads.get(sessionID)?.get(file)?.read - }) - - const assert = Effect.fn("FileTime.assert")(function* (sessionID: SessionID, filepath: string) { - if (disableCheck) return - filepath = AppFileSystem.normalizePath(filepath) - - const reads = (yield* InstanceState.get(state)).reads - const time = reads.get(sessionID)?.get(filepath) - if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`) - - const next = yield* stamp(filepath) - const changed = next.mtime !== time.mtime || next.size !== time.size - if (!changed) return - - throw new Error( - `File ${filepath} has been modified since it was last read.\nLast modification: ${new Date(next.mtime ?? next.read.getTime()).toISOString()}\nLast read: ${time.read.toISOString()}\n\nPlease read the file again before modifying it.`, - ) - }) - - const withLock = Effect.fn("FileTime.withLock")(function* (filepath: string, fn: () => Effect.Effect) { - return yield* fn().pipe((yield* getLock(filepath)).withPermits(1)) - }) - - return Service.of({ read, get, assert, withLock }) - }), - ).pipe(Layer.orDie) - - export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) +export type Stamp = { + readonly read: Date + readonly mtime: number | undefined + readonly size: number | undefined } + +const session = (reads: Map>, sessionID: SessionID) => { + const value = reads.get(sessionID) + if (value) return value + + const next = new Map() + reads.set(sessionID, next) + return next +} + +interface State { + reads: Map> + locks: Map +} + +export interface Interface { + readonly read: (sessionID: SessionID, file: string) => Effect.Effect + readonly get: (sessionID: SessionID, file: string) => Effect.Effect + readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect + readonly withLock: (filepath: string, fn: () => Effect.Effect) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/FileTime") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fsys = yield* AppFileSystem.Service + const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK + + const stamp = Effect.fnUntraced(function* (file: string) { + const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.void)) + return { + read: yield* DateTime.nowAsDate, + mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined, + size: info ? Number(info.size) : undefined, + } + }) + const state = yield* InstanceState.make( + Effect.fn("FileTime.state")(() => + Effect.succeed({ + reads: new Map>(), + locks: new Map(), + }), + ), + ) + + const getLock = Effect.fn("FileTime.lock")(function* (filepath: string) { + filepath = AppFileSystem.normalizePath(filepath) + const locks = (yield* InstanceState.get(state)).locks + const lock = locks.get(filepath) + if (lock) return lock + + const next = Semaphore.makeUnsafe(1) + locks.set(filepath, next) + return next + }) + + const read = Effect.fn("FileTime.read")(function* (sessionID: SessionID, file: string) { + file = AppFileSystem.normalizePath(file) + const reads = (yield* InstanceState.get(state)).reads + log.info("read", { sessionID, file }) + session(reads, sessionID).set(file, yield* stamp(file)) + }) + + const get = Effect.fn("FileTime.get")(function* (sessionID: SessionID, file: string) { + file = AppFileSystem.normalizePath(file) + const reads = (yield* InstanceState.get(state)).reads + return reads.get(sessionID)?.get(file)?.read + }) + + const assert = Effect.fn("FileTime.assert")(function* (sessionID: SessionID, filepath: string) { + if (disableCheck) return + filepath = AppFileSystem.normalizePath(filepath) + + const reads = (yield* InstanceState.get(state)).reads + const time = reads.get(sessionID)?.get(filepath) + if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`) + + const next = yield* stamp(filepath) + const changed = next.mtime !== time.mtime || next.size !== time.size + if (!changed) return + + throw new Error( + `File ${filepath} has been modified since it was last read.\nLast modification: ${new Date(next.mtime ?? next.read.getTime()).toISOString()}\nLast read: ${time.read.toISOString()}\n\nPlease read the file again before modifying it.`, + ) + }) + + const withLock = Effect.fn("FileTime.withLock")(function* (filepath: string, fn: () => Effect.Effect) { + return yield* fn().pipe((yield* getLock(filepath)).withPermits(1)) + }) + + return Service.of({ read, get, assert, withLock }) + }), +).pipe(Layer.orDie) + +export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) + +export * as FileTime from "./time" From 1089fa041561d76a58d72e464d95219af682eb8c Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 19:50:32 -0400 Subject: [PATCH 011/335] refactor: unwrap ServerProxy namespace + self-reexport (#22969) --- packages/opencode/src/server/proxy.ts | 146 +++++++++++++------------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/packages/opencode/src/server/proxy.ts b/packages/opencode/src/server/proxy.ts index 07703fdc80..22bc89baf2 100644 --- a/packages/opencode/src/server/proxy.ts +++ b/packages/opencode/src/server/proxy.ts @@ -101,83 +101,83 @@ const app = (upgrade: UpgradeWebSocket) => }), ) -export namespace ServerProxy { - const log = Log.Default.clone().tag("service", "server-proxy") +const log = Log.Default.clone().tag("service", "server-proxy") - export async function http( - url: string | URL, - extra: HeadersInit | undefined, - req: Request, - workspaceID: WorkspaceID, - ) { - if (!Workspace.isSyncing(workspaceID)) { - return new Response(`broken sync connection for workspace: ${workspaceID}`, { - status: 503, - headers: { - "content-type": "text/plain; charset=utf-8", - }, - }) - } - - return fetch( - new Request(url, { - method: req.method, - headers: headers(req, extra), - body: req.method === "GET" || req.method === "HEAD" ? undefined : req.body, - redirect: "manual", - signal: req.signal, - }), - ).then((res) => { - const sync = Fence.parse(res.headers) - const next = new Headers(res.headers) - next.delete("content-encoding") - next.delete("content-length") - - const done = sync ? Fence.wait(workspaceID, sync, req.signal) : Promise.resolve() - - return done.then(async () => { - console.log("proxy http response", { - method: req.method, - request: req.url, - url: String(url), - status: res.status, - statusText: res.statusText, - }) - return new Response(res.body, { - status: res.status, - statusText: res.statusText, - headers: next, - }) - }) +export async function http( + url: string | URL, + extra: HeadersInit | undefined, + req: Request, + workspaceID: WorkspaceID, +) { + if (!Workspace.isSyncing(workspaceID)) { + return new Response(`broken sync connection for workspace: ${workspaceID}`, { + status: 503, + headers: { + "content-type": "text/plain; charset=utf-8", + }, }) } - export function websocket( - upgrade: UpgradeWebSocket, - target: string | URL, - extra: HeadersInit | undefined, - req: Request, - env: unknown, - ) { - const proxy = new URL(req.url) - proxy.pathname = "/__workspace_ws" - proxy.search = "" - const next = new Headers(req.headers) - next.set("x-opencode-proxy-url", socket(target)) - for (const [key, value] of new Headers(extra).entries()) { - next.set(key, value) - } - log.info("proxy websocket", { - request: req.url, - target: String(target), - }) - return app(upgrade).fetch( - new Request(proxy, { + return fetch( + new Request(url, { + method: req.method, + headers: headers(req, extra), + body: req.method === "GET" || req.method === "HEAD" ? undefined : req.body, + redirect: "manual", + signal: req.signal, + }), + ).then((res) => { + const sync = Fence.parse(res.headers) + const next = new Headers(res.headers) + next.delete("content-encoding") + next.delete("content-length") + + const done = sync ? Fence.wait(workspaceID, sync, req.signal) : Promise.resolve() + + return done.then(async () => { + console.log("proxy http response", { method: req.method, + request: req.url, + url: String(url), + status: res.status, + statusText: res.statusText, + }) + return new Response(res.body, { + status: res.status, + statusText: res.statusText, headers: next, - signal: req.signal, - }), - env as never, - ) - } + }) + }) + }) } + +export function websocket( + upgrade: UpgradeWebSocket, + target: string | URL, + extra: HeadersInit | undefined, + req: Request, + env: unknown, +) { + const proxy = new URL(req.url) + proxy.pathname = "/__workspace_ws" + proxy.search = "" + const next = new Headers(req.headers) + next.set("x-opencode-proxy-url", socket(target)) + for (const [key, value] of new Headers(extra).entries()) { + next.set(key, value) + } + log.info("proxy websocket", { + request: req.url, + target: String(target), + }) + return app(upgrade).fetch( + new Request(proxy, { + method: req.method, + headers: next, + signal: req.signal, + }), + env as never, + ) +} + +export * as ServerProxy from "./proxy" From c03fa362572d8108d2d76c2a18bbf616a7345dac Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 19:51:01 -0400 Subject: [PATCH 012/335] refactor: unwrap Server namespace + self-reexport (#22970) --- packages/opencode/src/server/server.ts | 180 ++++++++++++------------- 1 file changed, 90 insertions(+), 90 deletions(-) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index fc3b399f79..892a99a77c 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -17,37 +17,22 @@ globalThis.AI_SDK_LOG_WARNINGS = false initProjectors() -export namespace Server { - const log = Log.create({ service: "server" }) +const log = Log.create({ service: "server" }) - export type Listener = { - hostname: string - port: number - url: URL - stop: (close?: boolean) => Promise - } +export type Listener = { + hostname: string + port: number + url: URL + stop: (close?: boolean) => Promise +} - export const Default = lazy(() => create({})) +export const Default = lazy(() => create({})) - function create(opts: { cors?: string[] }) { - const app = new Hono() - const runtime = adapter.create(app) - - if (Flag.OPENCODE_WORKSPACE_ID) { - return { - app: app - .onError(ErrorMiddleware) - .use(AuthMiddleware) - .use(LoggerMiddleware) - .use(CompressionMiddleware) - .use(CorsMiddleware(opts)) - .use(FenceMiddleware) - .route("/", ControlPlaneRoutes()) - .route("/", InstanceRoutes(runtime.upgradeWebSocket)), - runtime, - } - } +function create(opts: { cors?: string[] }) { + const app = new Hono() + const runtime = adapter.create(app) + if (Flag.OPENCODE_WORKSPACE_ID) { return { app: app .onError(ErrorMiddleware) @@ -55,73 +40,88 @@ export namespace Server { .use(LoggerMiddleware) .use(CompressionMiddleware) .use(CorsMiddleware(opts)) + .use(FenceMiddleware) .route("/", ControlPlaneRoutes()) - .route("/", InstanceRoutes(runtime.upgradeWebSocket)) - .route("/", UIRoutes()), + .route("/", InstanceRoutes(runtime.upgradeWebSocket)), runtime, } } - export async function openapi() { - // Build a fresh app with all routes registered directly so - // hono-openapi can see describeRoute metadata (`.route()` wraps - // handlers when the sub-app has a custom errorHandler, which - // strips the metadata symbol). - const { app } = create({}) - const result = await generateSpecs(app, { - documentation: { - info: { - title: "opencode", - version: "1.0.0", - description: "opencode api", - }, - openapi: "3.1.1", - }, - }) - return result - } - - export let url: URL - - export async function listen(opts: { - port: number - hostname: string - mdns?: boolean - mdnsDomain?: string - cors?: string[] - }): Promise { - const built = create(opts) - const server = await built.runtime.listen(opts) - - const next = new URL("http://localhost") - next.hostname = opts.hostname - next.port = String(server.port) - url = next - - const mdns = - opts.mdns && - server.port && - opts.hostname !== "127.0.0.1" && - opts.hostname !== "localhost" && - opts.hostname !== "::1" - if (mdns) { - MDNS.publish(server.port, opts.mdnsDomain) - } else if (opts.mdns) { - log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish") - } - - let closing: Promise | undefined - return { - hostname: opts.hostname, - port: server.port, - url: next, - stop(close?: boolean) { - closing ??= (async () => { - if (mdns) MDNS.unpublish() - await server.stop(close) - })() - return closing - }, - } + return { + app: app + .onError(ErrorMiddleware) + .use(AuthMiddleware) + .use(LoggerMiddleware) + .use(CompressionMiddleware) + .use(CorsMiddleware(opts)) + .route("/", ControlPlaneRoutes()) + .route("/", InstanceRoutes(runtime.upgradeWebSocket)) + .route("/", UIRoutes()), + runtime, } } + +export async function openapi() { + // Build a fresh app with all routes registered directly so + // hono-openapi can see describeRoute metadata (`.route()` wraps + // handlers when the sub-app has a custom errorHandler, which + // strips the metadata symbol). + const { app } = create({}) + const result = await generateSpecs(app, { + documentation: { + info: { + title: "opencode", + version: "1.0.0", + description: "opencode api", + }, + openapi: "3.1.1", + }, + }) + return result +} + +export let url: URL + +export async function listen(opts: { + port: number + hostname: string + mdns?: boolean + mdnsDomain?: string + cors?: string[] +}): Promise { + const built = create(opts) + const server = await built.runtime.listen(opts) + + const next = new URL("http://localhost") + next.hostname = opts.hostname + next.port = String(server.port) + url = next + + const mdns = + opts.mdns && + server.port && + opts.hostname !== "127.0.0.1" && + opts.hostname !== "localhost" && + opts.hostname !== "::1" + if (mdns) { + MDNS.publish(server.port, opts.mdnsDomain) + } else if (opts.mdns) { + log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish") + } + + let closing: Promise | undefined + return { + hostname: opts.hostname, + port: server.port, + url: next, + stop(close?: boolean) { + closing ??= (async () => { + if (mdns) MDNS.unpublish() + await server.stop(close) + })() + return closing + }, + } +} + +export * as Server from "./server" From 5d47ea091879b026b8efb9d09af06deb0643e46a Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 19:52:04 -0400 Subject: [PATCH 013/335] refactor: unwrap ConfigMCP namespace + self-reexport (#22948) --- .../opencode/src/cli/cmd/tui/context/kv.tsx | 2 +- packages/opencode/src/cli/error.ts | 8 +- packages/opencode/src/config/mcp.ts | 130 +++++++++--------- packages/opencode/src/lsp/lsp.ts | 9 +- packages/opencode/src/npm/index.ts | 13 +- packages/opencode/src/provider/provider.ts | 6 +- packages/opencode/src/session/session.ts | 22 +-- packages/opencode/src/tool/tool.ts | 2 +- packages/opencode/src/util/filesystem.ts | 2 +- packages/opencode/test/config/config.test.ts | 2 +- 10 files changed, 104 insertions(+), 92 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/kv.tsx b/packages/opencode/src/cli/cmd/tui/context/kv.tsx index 39e976b0e5..803752e766 100644 --- a/packages/opencode/src/cli/cmd/tui/context/kv.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/kv.tsx @@ -12,7 +12,7 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ const [store, setStore] = createStore>() const filePath = path.join(Global.Path.state, "kv.json") - Filesystem.readJson(filePath) + Filesystem.readJson>(filePath) .then((x) => { setStore(x) }) diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts index 89b557e2d2..f286b5166f 100644 --- a/packages/opencode/src/cli/error.ts +++ b/packages/opencode/src/cli/error.ts @@ -28,10 +28,10 @@ export function FormatError(input: unknown) { // ProviderModelNotFoundError: { providerID: string, modelID: string, suggestions?: string[] } if (NamedError.hasName(input, "ProviderModelNotFoundError")) { const data = (input as ErrorLike).data - const suggestions = data?.suggestions as string[] | undefined + const suggestions: string[] = Array.isArray(data?.suggestions) ? data.suggestions : [] return [ `Model not found: ${data?.providerID}/${data?.modelID}`, - ...(Array.isArray(suggestions) && suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []), + ...(suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []), `Try: \`opencode models\` to list available models`, `Or check your config (opencode.json) provider/model names`, ].join("\n") @@ -64,10 +64,10 @@ export function FormatError(input: unknown) { const data = (input as ErrorLike).data const path = data?.path const message = data?.message - const issues = data?.issues as Array<{ message: string; path: string[] }> | undefined + const issues: Array<{ message: string; path: string[] }> = Array.isArray(data?.issues) ? data.issues : [] return [ `Configuration is invalid${path && path !== "config" ? ` at ${path}` : ""}` + (message ? `: ${message}` : ""), - ...(issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ?? []), + ...issues.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")), ].join("\n") } diff --git a/packages/opencode/src/config/mcp.ts b/packages/opencode/src/config/mcp.ts index fb8f8caa41..fda933b421 100644 --- a/packages/opencode/src/config/mcp.ts +++ b/packages/opencode/src/config/mcp.ts @@ -1,70 +1,70 @@ import z from "zod" -export namespace ConfigMCP { - export const Local = z - .object({ - type: z.literal("local").describe("Type of MCP server connection"), - command: z.string().array().describe("Command and arguments to run the MCP server"), - environment: z - .record(z.string(), z.string()) - .optional() - .describe("Environment variables to set when running the MCP server"), - enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), - timeout: z - .number() - .int() - .positive() - .optional() - .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), - }) - .strict() - .meta({ - ref: "McpLocalConfig", - }) +export const Local = z + .object({ + type: z.literal("local").describe("Type of MCP server connection"), + command: z.string().array().describe("Command and arguments to run the MCP server"), + environment: z + .record(z.string(), z.string()) + .optional() + .describe("Environment variables to set when running the MCP server"), + enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), + timeout: z + .number() + .int() + .positive() + .optional() + .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), + }) + .strict() + .meta({ + ref: "McpLocalConfig", + }) - export const OAuth = z - .object({ - clientId: z - .string() - .optional() - .describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."), - clientSecret: z.string().optional().describe("OAuth client secret (if required by the authorization server)"), - scope: z.string().optional().describe("OAuth scopes to request during authorization"), - redirectUri: z - .string() - .optional() - .describe("OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback)."), - }) - .strict() - .meta({ - ref: "McpOAuthConfig", - }) - export type OAuth = z.infer +export const OAuth = z + .object({ + clientId: z + .string() + .optional() + .describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."), + clientSecret: z.string().optional().describe("OAuth client secret (if required by the authorization server)"), + scope: z.string().optional().describe("OAuth scopes to request during authorization"), + redirectUri: z + .string() + .optional() + .describe("OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback)."), + }) + .strict() + .meta({ + ref: "McpOAuthConfig", + }) +export type OAuth = z.infer - export const Remote = z - .object({ - type: z.literal("remote").describe("Type of MCP server connection"), - url: z.string().describe("URL of the remote MCP server"), - enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), - headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"), - oauth: z - .union([OAuth, z.literal(false)]) - .optional() - .describe( - "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.", - ), - timeout: z - .number() - .int() - .positive() - .optional() - .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), - }) - .strict() - .meta({ - ref: "McpRemoteConfig", - }) +export const Remote = z + .object({ + type: z.literal("remote").describe("Type of MCP server connection"), + url: z.string().describe("URL of the remote MCP server"), + enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), + headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"), + oauth: z + .union([OAuth, z.literal(false)]) + .optional() + .describe( + "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.", + ), + timeout: z + .number() + .int() + .positive() + .optional() + .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), + }) + .strict() + .meta({ + ref: "McpRemoteConfig", + }) - export const Info = z.discriminatedUnion("type", [Local, Remote]) - export type Info = z.infer -} +export const Info = z.discriminatedUnion("type", [Local, Remote]) +export type Info = z.infer + +export * as ConfigMCP from "./mcp" diff --git a/packages/opencode/src/lsp/lsp.ts b/packages/opencode/src/lsp/lsp.ts index d4d1e75634..d895e73256 100644 --- a/packages/opencode/src/lsp/lsp.ts +++ b/packages/opencode/src/lsp/lsp.ts @@ -440,12 +440,11 @@ export const layer = Layer.effect( const workspaceSymbol = Effect.fn("LSP.workspaceSymbol")(function* (query: string) { const results = yield* runAll((client) => client.connection - .sendRequest("workspace/symbol", { query }) - .then((result: any) => result.filter((x: Symbol) => kinds.includes(x.kind))) - .then((result: any) => result.slice(0, 10)) - .catch(() => []), + .sendRequest("workspace/symbol", { query }) + .then((result) => result.filter((x) => kinds.includes(x.kind)).slice(0, 10)) + .catch(() => [] as Symbol[]), ) - return results.flat() as Symbol[] + return results.flat() }) const prepareCallHierarchy = Effect.fn("LSP.prepareCallHierarchy")(function* (input: LocInput) { diff --git a/packages/opencode/src/npm/index.ts b/packages/opencode/src/npm/index.ts index 174df12974..425b27f420 100644 --- a/packages/opencode/src/npm/index.ts +++ b/packages/opencode/src/npm/index.ts @@ -124,8 +124,17 @@ export async function install(dir: string) { return } - const pkg = await Filesystem.readJson(path.join(dir, "package.json")).catch(() => ({})) - const lock = await Filesystem.readJson(path.join(dir, "package-lock.json")).catch(() => ({})) + type PackageDeps = Record + type PackageJson = { + dependencies?: PackageDeps + devDependencies?: PackageDeps + peerDependencies?: PackageDeps + optionalDependencies?: PackageDeps + } + const pkg: PackageJson = await Filesystem.readJson(path.join(dir, "package.json")).catch(() => ({})) + const lock: { packages?: Record } = await Filesystem.readJson<{ + packages?: Record + }>(path.join(dir, "package-lock.json")).catch(() => ({})) const declared = new Set([ ...Object.keys(pkg.dependencies || {}), diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 43ae9a5e9f..a7297634e7 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -547,12 +547,14 @@ function custom(dep: CustomDep): Record { }, async getModel(sdk: any, modelID: string, options?: Record) { if (modelID.startsWith("duo-workflow-")) { - const workflowRef = options?.workflowRef as string | undefined + const workflowRef = typeof options?.workflowRef === "string" ? options.workflowRef : undefined // Use the static mapping if it exists, otherwise use duo-workflow with selectedModelRef const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow" + const workflowDefinition = + typeof options?.workflowDefinition === "string" ? options.workflowDefinition : undefined const model = sdk.workflowChat(sdkModelID, { featureFlags, - workflowDefinition: options?.workflowDefinition as string | undefined, + workflowDefinition, }) if (workflowRef) { model.selectedModelRef = workflowRef diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 8c5fc29e4a..a453b19815 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -272,16 +272,18 @@ export const getUsage = (input: { model: Provider.Model; usage: LanguageModelUsa input.usage.inputTokenDetails?.cacheReadTokens ?? input.usage.cachedInputTokens ?? 0, ) const cacheWriteInputTokens = safe( - (input.usage.inputTokenDetails?.cacheWriteTokens ?? - input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? - // google-vertex-anthropic returns metadata under "vertex" key - // (AnthropicMessagesLanguageModel custom provider key from 'vertex.anthropic.messages') - input.metadata?.["vertex"]?.["cacheCreationInputTokens"] ?? - // @ts-expect-error - input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ?? - // @ts-expect-error - input.metadata?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ?? - 0) as number, + Number( + input.usage.inputTokenDetails?.cacheWriteTokens ?? + input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? + // google-vertex-anthropic returns metadata under "vertex" key + // (AnthropicMessagesLanguageModel custom provider key from 'vertex.anthropic.messages') + input.metadata?.["vertex"]?.["cacheCreationInputTokens"] ?? + // @ts-expect-error + input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ?? + // @ts-expect-error + input.metadata?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ?? + 0, + ), ) // AI SDK v6 normalized inputTokens to include cached tokens across all providers diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index 0ea0435fb1..179149afd2 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -19,7 +19,7 @@ export type Context = { agent: string abort: AbortSignal callID?: string - extra?: { [key: string]: any } + extra?: { [key: string]: unknown } messages: MessageV2.WithParts[] metadata(input: { title?: string; metadata?: M }): Effect.Effect ask(input: Omit): Effect.Effect diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 3ff2c6e3f4..6c4d455224 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -39,7 +39,7 @@ export async function readText(p: string): Promise { return readFile(p, "utf-8") } -export async function readJson(p: string): Promise { +export async function readJson(p: string): Promise { return JSON.parse(await readFile(p, "utf-8")) } diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index c41f395e51..3e90842e18 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -757,7 +757,7 @@ test("updates config and writes to file", async () => { const newConfig = { model: "updated/model" } await save(newConfig as any) - const writtenConfig = await Filesystem.readJson(path.join(tmp.path, "config.json")) + const writtenConfig = await Filesystem.readJson<{ model: string }>(path.join(tmp.path, "config.json")) expect(writtenConfig.model).toBe("updated/model") }, }) From f9aa3d77cd543ad3a46f86e36a2908f0cc2e652f Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 16 Apr 2026 23:53:10 +0000 Subject: [PATCH 014/335] chore: generate --- packages/opencode/src/config/mcp.ts | 4 +--- packages/opencode/src/server/proxy.ts | 7 +------ 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/config/mcp.ts b/packages/opencode/src/config/mcp.ts index fda933b421..5036cd6e4f 100644 --- a/packages/opencode/src/config/mcp.ts +++ b/packages/opencode/src/config/mcp.ts @@ -49,9 +49,7 @@ export const Remote = z oauth: z .union([OAuth, z.literal(false)]) .optional() - .describe( - "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.", - ), + .describe("OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection."), timeout: z .number() .int() diff --git a/packages/opencode/src/server/proxy.ts b/packages/opencode/src/server/proxy.ts index 22bc89baf2..9c1fd1f288 100644 --- a/packages/opencode/src/server/proxy.ts +++ b/packages/opencode/src/server/proxy.ts @@ -103,12 +103,7 @@ const app = (upgrade: UpgradeWebSocket) => const log = Log.Default.clone().tag("service", "server-proxy") -export async function http( - url: string | URL, - extra: HeadersInit | undefined, - req: Request, - workspaceID: WorkspaceID, -) { +export async function http(url: string | URL, extra: HeadersInit | undefined, req: Request, workspaceID: WorkspaceID) { if (!Workspace.isSyncing(workspaceID)) { return new Response(`broken sync connection for workspace: ${workspaceID}`, { status: 503, From bae80af1b4620961664076bd257c22b88b57eeaf Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:00:15 -0400 Subject: [PATCH 015/335] refactor: unwrap Workspace namespace + self-reexport (#22934) --- .../opencode/src/control-plane/workspace.ts | 886 +++++++++--------- 1 file changed, 443 insertions(+), 443 deletions(-) diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index 9c1c4c8960..3af11707e8 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -26,173 +26,239 @@ import { AppRuntime } from "@/effect/app-runtime" import { EventSequenceTable } from "@/sync/event.sql" import { waitEvent } from "./util" -export namespace Workspace { - export const Info = WorkspaceInfo.meta({ - ref: "Workspace", - }) - export type Info = z.infer +export const Info = WorkspaceInfo.meta({ + ref: "Workspace", +}) +export type Info = z.infer - export const ConnectionStatus = z.object({ - workspaceID: WorkspaceID.zod, - status: z.enum(["connected", "connecting", "disconnected", "error"]), - error: z.string().optional(), - }) - export type ConnectionStatus = z.infer +export const ConnectionStatus = z.object({ + workspaceID: WorkspaceID.zod, + status: z.enum(["connected", "connecting", "disconnected", "error"]), + error: z.string().optional(), +}) +export type ConnectionStatus = z.infer - const Restore = z.object({ - workspaceID: WorkspaceID.zod, - sessionID: SessionID.zod, - total: z.number().int().min(0), - step: z.number().int().min(0), - }) +const Restore = z.object({ + workspaceID: WorkspaceID.zod, + sessionID: SessionID.zod, + total: z.number().int().min(0), + step: z.number().int().min(0), +}) - export const Event = { - Ready: BusEvent.define( - "workspace.ready", - z.object({ - name: z.string(), - }), - ), - Failed: BusEvent.define( - "workspace.failed", - z.object({ - message: z.string(), - }), - ), - Restore: BusEvent.define("workspace.restore", Restore), - Status: BusEvent.define("workspace.status", ConnectionStatus), +export const Event = { + Ready: BusEvent.define( + "workspace.ready", + z.object({ + name: z.string(), + }), + ), + Failed: BusEvent.define( + "workspace.failed", + z.object({ + message: z.string(), + }), + ), + Restore: BusEvent.define("workspace.restore", Restore), + Status: BusEvent.define("workspace.status", ConnectionStatus), +} + +function fromRow(row: typeof WorkspaceTable.$inferSelect): Info { + return { + id: row.id, + type: row.type, + branch: row.branch, + name: row.name, + directory: row.directory, + extra: row.extra, + projectID: row.project_id, + } +} + +const CreateInput = z.object({ + id: WorkspaceID.zod.optional(), + type: Info.shape.type, + branch: Info.shape.branch, + projectID: ProjectID.zod, + extra: Info.shape.extra, +}) + +export const create = fn(CreateInput, async (input) => { + const id = WorkspaceID.ascending(input.id) + const adaptor = await getAdaptor(input.projectID, input.type) + + const config = await adaptor.configure({ ...input, id, name: Slug.create(), directory: null }) + + const info: Info = { + id, + type: config.type, + branch: config.branch ?? null, + name: config.name ?? null, + directory: config.directory ?? null, + extra: config.extra ?? null, + projectID: input.projectID, } - function fromRow(row: typeof WorkspaceTable.$inferSelect): Info { - return { - id: row.id, - type: row.type, - branch: row.branch, - name: row.name, - directory: row.directory, - extra: row.extra, - projectID: row.project_id, - } - } - - const CreateInput = z.object({ - id: WorkspaceID.zod.optional(), - type: Info.shape.type, - branch: Info.shape.branch, - projectID: ProjectID.zod, - extra: Info.shape.extra, + Database.use((db) => { + db.insert(WorkspaceTable) + .values({ + id: info.id, + type: info.type, + branch: info.branch, + name: info.name, + directory: info.directory, + extra: info.extra, + project_id: info.projectID, + }) + .run() }) - export const create = fn(CreateInput, async (input) => { - const id = WorkspaceID.ascending(input.id) - const adaptor = await getAdaptor(input.projectID, input.type) + const env = { + OPENCODE_AUTH_CONTENT: JSON.stringify(await AppRuntime.runPromise(Auth.Service.use((auth) => auth.all()))), + OPENCODE_WORKSPACE_ID: config.id, + OPENCODE_EXPERIMENTAL_WORKSPACES: "true", + } + await adaptor.create(config, env) - const config = await adaptor.configure({ ...input, id, name: Slug.create(), directory: null }) + startSync(info) - const info: Info = { - id, - type: config.type, - branch: config.branch ?? null, - name: config.name ?? null, - directory: config.directory ?? null, - extra: config.extra ?? null, - projectID: input.projectID, - } + await waitEvent({ + timeout: TIMEOUT, + fn(event) { + if (event.workspace === info.id && event.payload.type === Event.Status.type) { + const { status } = event.payload.properties + return status === "error" || status === "connected" + } + return false + }, + }) - Database.use((db) => { - db.insert(WorkspaceTable) - .values({ - id: info.id, - type: info.type, - branch: info.branch, - name: info.name, - directory: info.directory, - extra: info.extra, - project_id: info.projectID, - }) - .run() - }) + return info +}) - const env = { - OPENCODE_AUTH_CONTENT: JSON.stringify(await AppRuntime.runPromise(Auth.Service.use((auth) => auth.all()))), - OPENCODE_WORKSPACE_ID: config.id, - OPENCODE_EXPERIMENTAL_WORKSPACES: "true", - } - await adaptor.create(config, env) +const SessionRestoreInput = z.object({ + workspaceID: WorkspaceID.zod, + sessionID: SessionID.zod, +}) - startSync(info) +export const sessionRestore = fn(SessionRestoreInput, async (input) => { + log.info("session restore requested", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + }) + try { + const space = await get(input.workspaceID) + if (!space) throw new Error(`Workspace not found: ${input.workspaceID}`) - await waitEvent({ - timeout: TIMEOUT, - fn(event) { - if (event.workspace === info.id && event.payload.type === Event.Status.type) { - const { status } = event.payload.properties - return status === "error" || status === "connected" - } - return false + const adaptor = await getAdaptor(space.projectID, space.type) + const target = await adaptor.target(space) + + // Need to switch the workspace of the session + SyncEvent.run(Session.Event.Updated, { + sessionID: input.sessionID, + info: { + workspaceID: input.workspaceID, }, }) - return info - }) + const rows = Database.use((db) => + db + .select({ + id: EventTable.id, + aggregateID: EventTable.aggregate_id, + seq: EventTable.seq, + type: EventTable.type, + data: EventTable.data, + }) + .from(EventTable) + .where(eq(EventTable.aggregate_id, input.sessionID)) + .orderBy(asc(EventTable.seq)) + .all(), + ) + if (rows.length === 0) throw new Error(`No events found for session: ${input.sessionID}`) - const SessionRestoreInput = z.object({ - workspaceID: WorkspaceID.zod, - sessionID: SessionID.zod, - }) + const all = rows - export const sessionRestore = fn(SessionRestoreInput, async (input) => { - log.info("session restore requested", { + const size = 10 + const sets = Array.from({ length: Math.ceil(all.length / size) }, (_, i) => all.slice(i * size, (i + 1) * size)) + const total = sets.length + log.info("session restore prepared", { workspaceID: input.workspaceID, sessionID: input.sessionID, + workspaceType: space.type, + directory: space.directory, + target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory, + events: all.length, + batches: total, + first: all[0]?.seq, + last: all.at(-1)?.seq, }) - try { - const space = await get(input.workspaceID) - if (!space) throw new Error(`Workspace not found: ${input.workspaceID}`) - - const adaptor = await getAdaptor(space.projectID, space.type) - const target = await adaptor.target(space) - - // Need to switch the workspace of the session - SyncEvent.run(Session.Event.Updated, { - sessionID: input.sessionID, - info: { + GlobalBus.emit("event", { + directory: "global", + workspace: input.workspaceID, + payload: { + type: Event.Restore.type, + properties: { workspaceID: input.workspaceID, + sessionID: input.sessionID, + total, + step: 0, }, - }) - - const rows = Database.use((db) => - db - .select({ - id: EventTable.id, - aggregateID: EventTable.aggregate_id, - seq: EventTable.seq, - type: EventTable.type, - data: EventTable.data, - }) - .from(EventTable) - .where(eq(EventTable.aggregate_id, input.sessionID)) - .orderBy(asc(EventTable.seq)) - .all(), - ) - if (rows.length === 0) throw new Error(`No events found for session: ${input.sessionID}`) - - const all = rows - - const size = 10 - const sets = Array.from({ length: Math.ceil(all.length / size) }, (_, i) => all.slice(i * size, (i + 1) * size)) - const total = sets.length - log.info("session restore prepared", { + }, + }) + for (const [i, events] of sets.entries()) { + log.info("session restore batch starting", { workspaceID: input.workspaceID, sessionID: input.sessionID, - workspaceType: space.type, - directory: space.directory, + step: i + 1, + total, + events: events.length, + first: events[0]?.seq, + last: events.at(-1)?.seq, target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory, - events: all.length, - batches: total, - first: all[0]?.seq, - last: all.at(-1)?.seq, }) + if (target.type === "local") { + SyncEvent.replayAll(events) + log.info("session restore batch replayed locally", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + step: i + 1, + total, + events: events.length, + }) + } else { + const url = route(target.url, "/sync/replay") + const headers = new Headers(target.headers) + headers.set("content-type", "application/json") + const res = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify({ + directory: space.directory ?? "", + events, + }), + }) + if (!res.ok) { + const body = await res.text() + log.error("session restore batch failed", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + step: i + 1, + total, + status: res.status, + body, + }) + throw new Error( + `Failed to replay session ${input.sessionID} into workspace ${input.workspaceID}: HTTP ${res.status} ${body}`, + ) + } + log.info("session restore batch posted", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + step: i + 1, + total, + status: res.status, + }) + } GlobalBus.emit("event", { directory: "global", workspace: input.workspaceID, @@ -202,329 +268,263 @@ export namespace Workspace { workspaceID: input.workspaceID, sessionID: input.sessionID, total, - step: 0, + step: i + 1, }, }, }) - for (const [i, events] of sets.entries()) { - log.info("session restore batch starting", { - workspaceID: input.workspaceID, - sessionID: input.sessionID, - step: i + 1, - total, - events: events.length, - first: events[0]?.seq, - last: events.at(-1)?.seq, - target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory, + } + + log.info("session restore complete", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + batches: total, + }) + + return { + total, + } + } catch (err) { + log.error("session restore failed", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + error: errorData(err), + }) + throw err + } +}) + +export function list(project: Project.Info) { + const rows = Database.use((db) => + db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(), + ) + const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id)) + + for (const space of spaces) startSync(space) + return spaces +} + +function lookup(id: WorkspaceID) { + const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get()) + if (!row) return + return fromRow(row) +} + +export const get = fn(WorkspaceID.zod, async (id) => { + const space = lookup(id) + if (!space) return + startSync(space) + return space +}) + +export const remove = fn(WorkspaceID.zod, async (id) => { + const sessions = Database.use((db) => + db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.workspace_id, id)).all(), + ) + for (const session of sessions) { + await AppRuntime.runPromise(Session.Service.use((svc) => svc.remove(session.id))) + } + + const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get()) + + if (row) { + stopSync(id) + + const info = fromRow(row) + try { + const adaptor = await getAdaptor(info.projectID, row.type) + await adaptor.remove(info) + } catch { + log.error("adaptor not available when removing workspace", { type: row.type }) + } + Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run()) + return info + } +}) + +const connections = new Map() +const aborts = new Map() +const TIMEOUT = 5000 + +function setStatus(id: WorkspaceID, status: ConnectionStatus["status"], error?: string) { + const prev = connections.get(id) + if (prev?.status === status && prev?.error === error) return + const next = { workspaceID: id, status, error } + connections.set(id, next) + + if (status === "error") { + aborts.delete(id) + } + + GlobalBus.emit("event", { + directory: "global", + workspace: id, + payload: { + type: Event.Status.type, + properties: next, + }, + }) +} + +export function status(): ConnectionStatus[] { + return [...connections.values()] +} + +function synced(state: Record) { + const ids = Object.keys(state) + if (ids.length === 0) return true + + const done = Object.fromEntries( + Database.use((db) => + db + .select({ + id: EventSequenceTable.aggregate_id, + seq: EventSequenceTable.seq, }) - if (target.type === "local") { - SyncEvent.replayAll(events) - log.info("session restore batch replayed locally", { - workspaceID: input.workspaceID, - sessionID: input.sessionID, - step: i + 1, - total, - events: events.length, - }) - } else { - const url = route(target.url, "/sync/replay") - const headers = new Headers(target.headers) - headers.set("content-type", "application/json") - const res = await fetch(url, { - method: "POST", - headers, - body: JSON.stringify({ - directory: space.directory ?? "", - events, - }), - }) - if (!res.ok) { - const body = await res.text() - log.error("session restore batch failed", { - workspaceID: input.workspaceID, - sessionID: input.sessionID, - step: i + 1, - total, - status: res.status, - body, - }) - throw new Error( - `Failed to replay session ${input.sessionID} into workspace ${input.workspaceID}: HTTP ${res.status} ${body}`, - ) - } - log.info("session restore batch posted", { - workspaceID: input.workspaceID, - sessionID: input.sessionID, - step: i + 1, - total, - status: res.status, - }) + .from(EventSequenceTable) + .where(inArray(EventSequenceTable.aggregate_id, ids)) + .all(), + ).map((row) => [row.id, row.seq]), + ) as Record + + return ids.every((id) => { + return (done[id] ?? -1) >= state[id] + }) +} + +export async function isSyncing(workspaceID: WorkspaceID) { + return aborts.has(workspaceID) +} + +export async function waitForSync(workspaceID: WorkspaceID, state: Record, signal?: AbortSignal) { + if (synced(state)) return + + try { + await waitEvent({ + timeout: TIMEOUT, + signal, + fn(event) { + if (event.workspace !== workspaceID && event.payload.type !== "sync") { + return false } - GlobalBus.emit("event", { - directory: "global", - workspace: input.workspaceID, - payload: { - type: Event.Restore.type, - properties: { - workspaceID: input.workspaceID, - sessionID: input.sessionID, - total, - step: i + 1, - }, - }, - }) - } - - log.info("session restore complete", { - workspaceID: input.workspaceID, - sessionID: input.sessionID, - batches: total, - }) - - return { - total, - } - } catch (err) { - log.error("session restore failed", { - workspaceID: input.workspaceID, - sessionID: input.sessionID, - error: errorData(err), - }) - throw err - } - }) - - export function list(project: Project.Info) { - const rows = Database.use((db) => - db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(), - ) - const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id)) - - for (const space of spaces) startSync(space) - return spaces - } - - function lookup(id: WorkspaceID) { - const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get()) - if (!row) return - return fromRow(row) - } - - export const get = fn(WorkspaceID.zod, async (id) => { - const space = lookup(id) - if (!space) return - startSync(space) - return space - }) - - export const remove = fn(WorkspaceID.zod, async (id) => { - const sessions = Database.use((db) => - db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.workspace_id, id)).all(), - ) - for (const session of sessions) { - await AppRuntime.runPromise(Session.Service.use((svc) => svc.remove(session.id))) - } - - const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get()) - - if (row) { - stopSync(id) - - const info = fromRow(row) - try { - const adaptor = await getAdaptor(info.projectID, row.type) - await adaptor.remove(info) - } catch { - log.error("adaptor not available when removing workspace", { type: row.type }) - } - Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run()) - return info - } - }) - - const connections = new Map() - const aborts = new Map() - const TIMEOUT = 5000 - - function setStatus(id: WorkspaceID, status: ConnectionStatus["status"], error?: string) { - const prev = connections.get(id) - if (prev?.status === status && prev?.error === error) return - const next = { workspaceID: id, status, error } - connections.set(id, next) - - if (status === "error") { - aborts.delete(id) - } - - GlobalBus.emit("event", { - directory: "global", - workspace: id, - payload: { - type: Event.Status.type, - properties: next, + return synced(state) }, }) + } catch { + if (signal?.aborted) throw signal.reason ?? new Error("Request aborted") + throw new Error(`Timed out waiting for sync fence: ${JSON.stringify(state)}`) } +} - export function status(): ConnectionStatus[] { - return [...connections.values()] - } +const log = Log.create({ service: "workspace-sync" }) - function synced(state: Record) { - const ids = Object.keys(state) - if (ids.length === 0) return true +function route(url: string | URL, path: string) { + const next = new URL(url) + next.pathname = `${next.pathname.replace(/\/$/, "")}${path}` + next.search = "" + next.hash = "" + return next +} - const done = Object.fromEntries( - Database.use((db) => - db - .select({ - id: EventSequenceTable.aggregate_id, - seq: EventSequenceTable.seq, - }) - .from(EventSequenceTable) - .where(inArray(EventSequenceTable.aggregate_id, ids)) - .all(), - ).map((row) => [row.id, row.seq]), - ) as Record - - return ids.every((id) => { - return (done[id] ?? -1) >= state[id] - }) - } - - export async function isSyncing(workspaceID: WorkspaceID) { - return aborts.has(workspaceID) - } - - export async function waitForSync(workspaceID: WorkspaceID, state: Record, signal?: AbortSignal) { - if (synced(state)) return - - try { - await waitEvent({ - timeout: TIMEOUT, - signal, - fn(event) { - if (event.workspace !== workspaceID && event.payload.type !== "sync") { - return false - } - return synced(state) - }, - }) - } catch { - if (signal?.aborted) throw signal.reason ?? new Error("Request aborted") - throw new Error(`Timed out waiting for sync fence: ${JSON.stringify(state)}`) - } - } - - const log = Log.create({ service: "workspace-sync" }) - - function route(url: string | URL, path: string) { - const next = new URL(url) - next.pathname = `${next.pathname.replace(/\/$/, "")}${path}` - next.search = "" - next.hash = "" - return next - } - - async function syncWorkspace(space: Info, signal: AbortSignal) { - while (!signal.aborted) { - log.info("connecting to global sync", { workspace: space.name }) - setStatus(space.id, "connecting") - - const adaptor = await getAdaptor(space.projectID, space.type) - const target = await adaptor.target(space) - - if (target.type === "local") return - - const res = await fetch(route(target.url, "/global/event"), { - method: "GET", - headers: target.headers, - signal, - }).catch((err: unknown) => { - setStatus(space.id, "error", err instanceof Error ? err.message : String(err)) - - log.info("failed to connect to global sync", { - workspace: space.name, - error: err, - }) - return undefined - }) - - if (!res || !res.ok || !res.body) { - const error = !res ? "No response from global sync" : `Global sync HTTP ${res.status}` - log.info("failed to connect to global sync", { workspace: space.name, error }) - setStatus(space.id, "error", error) - await sleep(1000) - continue - } - - log.info("global sync connected", { workspace: space.name }) - setStatus(space.id, "connected") - - await parseSSE(res.body, signal, (evt: any) => { - try { - if (!("payload" in evt)) return - - if (evt.payload.type === "sync") { - SyncEvent.replay(evt.payload.syncEvent as SyncEvent.SerializedEvent) - } - - GlobalBus.emit("event", { - directory: evt.directory, - project: evt.project, - workspace: space.id, - payload: evt.payload, - }) - } catch (err) { - log.info("failed to replay global event", { - workspaceID: space.id, - error: err, - }) - } - }) - - log.info("disconnected from global sync: " + space.id) - setStatus(space.id, "disconnected") - - // TODO: Implement exponential backoff - await sleep(1000) - } - } - - async function startSync(space: Info) { - if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return +async function syncWorkspace(space: Info, signal: AbortSignal) { + while (!signal.aborted) { + log.info("connecting to global sync", { workspace: space.name }) + setStatus(space.id, "connecting") const adaptor = await getAdaptor(space.projectID, space.type) const target = await adaptor.target(space) - if (target.type === "local") { - void Filesystem.exists(target.directory).then((exists) => { - setStatus(space.id, exists ? "connected" : "error", exists ? undefined : "directory does not exist") + if (target.type === "local") return + + const res = await fetch(route(target.url, "/global/event"), { + method: "GET", + headers: target.headers, + signal, + }).catch((err: unknown) => { + setStatus(space.id, "error", err instanceof Error ? err.message : String(err)) + + log.info("failed to connect to global sync", { + workspace: space.name, + error: err, }) - return + return undefined + }) + + if (!res || !res.ok || !res.body) { + const error = !res ? "No response from global sync" : `Global sync HTTP ${res.status}` + log.info("failed to connect to global sync", { workspace: space.name, error }) + setStatus(space.id, "error", error) + await sleep(1000) + continue } - if (aborts.has(space.id)) return true + log.info("global sync connected", { workspace: space.name }) + setStatus(space.id, "connected") + await parseSSE(res.body, signal, (evt: any) => { + try { + if (!("payload" in evt)) return + + if (evt.payload.type === "sync") { + SyncEvent.replay(evt.payload.syncEvent as SyncEvent.SerializedEvent) + } + + GlobalBus.emit("event", { + directory: evt.directory, + project: evt.project, + workspace: space.id, + payload: evt.payload, + }) + } catch (err) { + log.info("failed to replay global event", { + workspaceID: space.id, + error: err, + }) + } + }) + + log.info("disconnected from global sync: " + space.id) setStatus(space.id, "disconnected") - const abort = new AbortController() - aborts.set(space.id, abort) - - void syncWorkspace(space, abort.signal).catch((error) => { - aborts.delete(space.id) - - setStatus(space.id, "error", String(error)) - log.warn("workspace listener failed", { - workspaceID: space.id, - error, - }) - }) - } - - function stopSync(id: WorkspaceID) { - aborts.get(id)?.abort() - aborts.delete(id) - connections.delete(id) + // TODO: Implement exponential backoff + await sleep(1000) } } + +async function startSync(space: Info) { + if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return + + const adaptor = await getAdaptor(space.projectID, space.type) + const target = await adaptor.target(space) + + if (target.type === "local") { + void Filesystem.exists(target.directory).then((exists) => { + setStatus(space.id, exists ? "connected" : "error", exists ? undefined : "directory does not exist") + }) + return + } + + if (aborts.has(space.id)) return true + + setStatus(space.id, "disconnected") + + const abort = new AbortController() + aborts.set(space.id, abort) + + void syncWorkspace(space, abort.signal).catch((error) => { + aborts.delete(space.id) + + setStatus(space.id, "error", String(error)) + log.warn("workspace listener failed", { + workspaceID: space.id, + error, + }) + }) +} + +function stopSync(id: WorkspaceID) { + aborts.get(id)?.abort() + aborts.delete(id) + connections.delete(id) +} + +export * as Workspace from "./workspace" From 4e27804160e7df606c27bdc72c1a8acae2304629 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:00:46 -0400 Subject: [PATCH 016/335] refactor: unwrap McpOAuthCallback namespace + self-reexport (#22943) --- packages/opencode/src/mcp/oauth-callback.ts | 338 ++++++++++---------- 1 file changed, 169 insertions(+), 169 deletions(-) diff --git a/packages/opencode/src/mcp/oauth-callback.ts b/packages/opencode/src/mcp/oauth-callback.ts index 3e6169517f..fbb43d3921 100644 --- a/packages/opencode/src/mcp/oauth-callback.ts +++ b/packages/opencode/src/mcp/oauth-callback.ts @@ -56,177 +56,177 @@ interface PendingAuth { timeout: ReturnType } -export namespace McpOAuthCallback { - let server: ReturnType | undefined - const pendingAuths = new Map() - // Reverse index: mcpName → oauthState, so cancelPending(mcpName) can - // find the right entry in pendingAuths (which is keyed by oauthState). - const mcpNameToState = new Map() +let server: ReturnType | undefined +const pendingAuths = new Map() +// Reverse index: mcpName → oauthState, so cancelPending(mcpName) can +// find the right entry in pendingAuths (which is keyed by oauthState). +const mcpNameToState = new Map() - const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes +const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes - function cleanupStateIndex(oauthState: string) { - for (const [name, state] of mcpNameToState) { - if (state === oauthState) { - mcpNameToState.delete(name) - break - } +function cleanupStateIndex(oauthState: string) { + for (const [name, state] of mcpNameToState) { + if (state === oauthState) { + mcpNameToState.delete(name) + break } } - - function handleRequest(req: import("http").IncomingMessage, res: import("http").ServerResponse) { - const url = new URL(req.url || "/", `http://localhost:${currentPort}`) - - if (url.pathname !== currentPath) { - res.writeHead(404) - res.end("Not found") - return - } - - const code = url.searchParams.get("code") - const state = url.searchParams.get("state") - const error = url.searchParams.get("error") - const errorDescription = url.searchParams.get("error_description") - - log.info("received oauth callback", { hasCode: !!code, state, error }) - - // Enforce state parameter presence - if (!state) { - const errorMsg = "Missing required state parameter - potential CSRF attack" - log.error("oauth callback missing state parameter", { url: url.toString() }) - res.writeHead(400, { "Content-Type": "text/html" }) - res.end(HTML_ERROR(errorMsg)) - return - } - - if (error) { - const errorMsg = errorDescription || error - if (pendingAuths.has(state)) { - const pending = pendingAuths.get(state)! - clearTimeout(pending.timeout) - pendingAuths.delete(state) - cleanupStateIndex(state) - pending.reject(new Error(errorMsg)) - } - res.writeHead(200, { "Content-Type": "text/html" }) - res.end(HTML_ERROR(errorMsg)) - return - } - - if (!code) { - res.writeHead(400, { "Content-Type": "text/html" }) - res.end(HTML_ERROR("No authorization code provided")) - return - } - - // Validate state parameter - if (!pendingAuths.has(state)) { - const errorMsg = "Invalid or expired state parameter - potential CSRF attack" - log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) }) - res.writeHead(400, { "Content-Type": "text/html" }) - res.end(HTML_ERROR(errorMsg)) - return - } - - const pending = pendingAuths.get(state)! - - clearTimeout(pending.timeout) - pendingAuths.delete(state) - cleanupStateIndex(state) - pending.resolve(code) - - res.writeHead(200, { "Content-Type": "text/html" }) - res.end(HTML_SUCCESS) - } - - export async function ensureRunning(redirectUri?: string): Promise { - // Parse the redirect URI to get port and path (uses defaults if not provided) - const { port, path } = parseRedirectUri(redirectUri) - - // If server is running on a different port/path, stop it first - if (server && (currentPort !== port || currentPath !== path)) { - log.info("stopping oauth callback server to reconfigure", { oldPort: currentPort, newPort: port }) - await stop() - } - - if (server) return - - const running = await isPortInUse(port) - if (running) { - log.info("oauth callback server already running on another instance", { port }) - return - } - - currentPort = port - currentPath = path - - server = createServer(handleRequest) - await new Promise((resolve, reject) => { - server!.listen(currentPort, () => { - log.info("oauth callback server started", { port: currentPort, path: currentPath }) - resolve() - }) - server!.on("error", reject) - }) - } - - export function waitForCallback(oauthState: string, mcpName?: string): Promise { - if (mcpName) mcpNameToState.set(mcpName, oauthState) - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - if (pendingAuths.has(oauthState)) { - pendingAuths.delete(oauthState) - if (mcpName) mcpNameToState.delete(mcpName) - reject(new Error("OAuth callback timeout - authorization took too long")) - } - }, CALLBACK_TIMEOUT_MS) - - pendingAuths.set(oauthState, { resolve, reject, timeout }) - }) - } - - export function cancelPending(mcpName: string): void { - // Look up the oauthState for this mcpName via the reverse index - const oauthState = mcpNameToState.get(mcpName) - const key = oauthState ?? mcpName - const pending = pendingAuths.get(key) - if (pending) { - clearTimeout(pending.timeout) - pendingAuths.delete(key) - mcpNameToState.delete(mcpName) - pending.reject(new Error("Authorization cancelled")) - } - } - - export async function isPortInUse(port: number = OAUTH_CALLBACK_PORT): Promise { - return new Promise((resolve) => { - const socket = createConnection(port, "127.0.0.1") - socket.on("connect", () => { - socket.destroy() - resolve(true) - }) - socket.on("error", () => { - resolve(false) - }) - }) - } - - export async function stop(): Promise { - if (server) { - await new Promise((resolve) => server!.close(() => resolve())) - server = undefined - log.info("oauth callback server stopped") - } - - for (const [_name, pending] of pendingAuths) { - clearTimeout(pending.timeout) - pending.reject(new Error("OAuth callback server stopped")) - } - pendingAuths.clear() - mcpNameToState.clear() - } - - export function isRunning(): boolean { - return server !== undefined - } } + +function handleRequest(req: import("http").IncomingMessage, res: import("http").ServerResponse) { + const url = new URL(req.url || "/", `http://localhost:${currentPort}`) + + if (url.pathname !== currentPath) { + res.writeHead(404) + res.end("Not found") + return + } + + const code = url.searchParams.get("code") + const state = url.searchParams.get("state") + const error = url.searchParams.get("error") + const errorDescription = url.searchParams.get("error_description") + + log.info("received oauth callback", { hasCode: !!code, state, error }) + + // Enforce state parameter presence + if (!state) { + const errorMsg = "Missing required state parameter - potential CSRF attack" + log.error("oauth callback missing state parameter", { url: url.toString() }) + res.writeHead(400, { "Content-Type": "text/html" }) + res.end(HTML_ERROR(errorMsg)) + return + } + + if (error) { + const errorMsg = errorDescription || error + if (pendingAuths.has(state)) { + const pending = pendingAuths.get(state)! + clearTimeout(pending.timeout) + pendingAuths.delete(state) + cleanupStateIndex(state) + pending.reject(new Error(errorMsg)) + } + res.writeHead(200, { "Content-Type": "text/html" }) + res.end(HTML_ERROR(errorMsg)) + return + } + + if (!code) { + res.writeHead(400, { "Content-Type": "text/html" }) + res.end(HTML_ERROR("No authorization code provided")) + return + } + + // Validate state parameter + if (!pendingAuths.has(state)) { + const errorMsg = "Invalid or expired state parameter - potential CSRF attack" + log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) }) + res.writeHead(400, { "Content-Type": "text/html" }) + res.end(HTML_ERROR(errorMsg)) + return + } + + const pending = pendingAuths.get(state)! + + clearTimeout(pending.timeout) + pendingAuths.delete(state) + cleanupStateIndex(state) + pending.resolve(code) + + res.writeHead(200, { "Content-Type": "text/html" }) + res.end(HTML_SUCCESS) +} + +export async function ensureRunning(redirectUri?: string): Promise { + // Parse the redirect URI to get port and path (uses defaults if not provided) + const { port, path } = parseRedirectUri(redirectUri) + + // If server is running on a different port/path, stop it first + if (server && (currentPort !== port || currentPath !== path)) { + log.info("stopping oauth callback server to reconfigure", { oldPort: currentPort, newPort: port }) + await stop() + } + + if (server) return + + const running = await isPortInUse(port) + if (running) { + log.info("oauth callback server already running on another instance", { port }) + return + } + + currentPort = port + currentPath = path + + server = createServer(handleRequest) + await new Promise((resolve, reject) => { + server!.listen(currentPort, () => { + log.info("oauth callback server started", { port: currentPort, path: currentPath }) + resolve() + }) + server!.on("error", reject) + }) +} + +export function waitForCallback(oauthState: string, mcpName?: string): Promise { + if (mcpName) mcpNameToState.set(mcpName, oauthState) + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + if (pendingAuths.has(oauthState)) { + pendingAuths.delete(oauthState) + if (mcpName) mcpNameToState.delete(mcpName) + reject(new Error("OAuth callback timeout - authorization took too long")) + } + }, CALLBACK_TIMEOUT_MS) + + pendingAuths.set(oauthState, { resolve, reject, timeout }) + }) +} + +export function cancelPending(mcpName: string): void { + // Look up the oauthState for this mcpName via the reverse index + const oauthState = mcpNameToState.get(mcpName) + const key = oauthState ?? mcpName + const pending = pendingAuths.get(key) + if (pending) { + clearTimeout(pending.timeout) + pendingAuths.delete(key) + mcpNameToState.delete(mcpName) + pending.reject(new Error("Authorization cancelled")) + } +} + +export async function isPortInUse(port: number = OAUTH_CALLBACK_PORT): Promise { + return new Promise((resolve) => { + const socket = createConnection(port, "127.0.0.1") + socket.on("connect", () => { + socket.destroy() + resolve(true) + }) + socket.on("error", () => { + resolve(false) + }) + }) +} + +export async function stop(): Promise { + if (server) { + await new Promise((resolve) => server!.close(() => resolve())) + server = undefined + log.info("oauth callback server stopped") + } + + for (const [_name, pending] of pendingAuths) { + clearTimeout(pending.timeout) + pending.reject(new Error("OAuth callback server stopped")) + } + pendingAuths.clear() + mcpNameToState.clear() +} + +export function isRunning(): boolean { + return server !== undefined +} + +export * as McpOAuthCallback from "./oauth-callback" From 19d15d9ff7826db276219bccce278f78b654a431 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:00:48 -0400 Subject: [PATCH 017/335] refactor: unwrap ConfigProvider namespace + self-reexport (#22949) --- packages/opencode/src/config/provider.ts | 230 +++++++++++------------ 1 file changed, 115 insertions(+), 115 deletions(-) diff --git a/packages/opencode/src/config/provider.ts b/packages/opencode/src/config/provider.ts index 09efedf497..877677519f 100644 --- a/packages/opencode/src/config/provider.ts +++ b/packages/opencode/src/config/provider.ts @@ -1,120 +1,120 @@ import z from "zod" -export namespace ConfigProvider { - export const Model = z - .object({ - id: z.string(), - name: z.string(), - family: z.string().optional(), - release_date: z.string(), - attachment: z.boolean(), - reasoning: z.boolean(), - temperature: z.boolean(), - tool_call: z.boolean(), - interleaved: z - .union([ - z.literal(true), - z - .object({ - field: z.enum(["reasoning_content", "reasoning_details"]), - }) - .strict(), - ]) - .optional(), - cost: z - .object({ - input: z.number(), - output: z.number(), - cache_read: z.number().optional(), - cache_write: z.number().optional(), - context_over_200k: z - .object({ - input: z.number(), - output: z.number(), - cache_read: z.number().optional(), - cache_write: z.number().optional(), - }) - .optional(), - }) - .optional(), - limit: z.object({ - context: z.number(), - input: z.number().optional(), +export const Model = z + .object({ + id: z.string(), + name: z.string(), + family: z.string().optional(), + release_date: z.string(), + attachment: z.boolean(), + reasoning: z.boolean(), + temperature: z.boolean(), + tool_call: z.boolean(), + interleaved: z + .union([ + z.literal(true), + z + .object({ + field: z.enum(["reasoning_content", "reasoning_details"]), + }) + .strict(), + ]) + .optional(), + cost: z + .object({ + input: z.number(), output: z.number(), - }), - modalities: z - .object({ - input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), - output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), - }) - .optional(), - experimental: z.boolean().optional(), - status: z.enum(["alpha", "beta", "deprecated"]).optional(), - provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(), - options: z.record(z.string(), z.any()), - headers: z.record(z.string(), z.string()).optional(), - variants: z - .record( - z.string(), - z - .object({ - disabled: z.boolean().optional().describe("Disable this variant for the model"), - }) - .catchall(z.any()), - ) - .optional() - .describe("Variant-specific configuration"), - }) - .partial() + cache_read: z.number().optional(), + cache_write: z.number().optional(), + context_over_200k: z + .object({ + input: z.number(), + output: z.number(), + cache_read: z.number().optional(), + cache_write: z.number().optional(), + }) + .optional(), + }) + .optional(), + limit: z.object({ + context: z.number(), + input: z.number().optional(), + output: z.number(), + }), + modalities: z + .object({ + input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), + output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), + }) + .optional(), + experimental: z.boolean().optional(), + status: z.enum(["alpha", "beta", "deprecated"]).optional(), + provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(), + options: z.record(z.string(), z.any()), + headers: z.record(z.string(), z.string()).optional(), + variants: z + .record( + z.string(), + z + .object({ + disabled: z.boolean().optional().describe("Disable this variant for the model"), + }) + .catchall(z.any()), + ) + .optional() + .describe("Variant-specific configuration"), + }) + .partial() - export const Info = z - .object({ - api: z.string().optional(), - name: z.string(), - env: z.array(z.string()), - id: z.string(), - npm: z.string().optional(), - whitelist: z.array(z.string()).optional(), - blacklist: z.array(z.string()).optional(), - options: z - .object({ - apiKey: z.string().optional(), - baseURL: z.string().optional(), - enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"), - setCacheKey: z.boolean().optional().describe("Enable promptCacheKey for this provider (default false)"), - timeout: z - .union([ - z - .number() - .int() - .positive() - .describe( - "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", - ), - z.literal(false).describe("Disable timeout for this provider entirely."), - ]) - .optional() - .describe( - "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", - ), - chunkTimeout: z - .number() - .int() - .positive() - .optional() - .describe( - "Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.", - ), - }) - .catchall(z.any()) - .optional(), - models: z.record(z.string(), Model).optional(), - }) - .partial() - .strict() - .meta({ - ref: "ProviderConfig", - }) +export const Info = z + .object({ + api: z.string().optional(), + name: z.string(), + env: z.array(z.string()), + id: z.string(), + npm: z.string().optional(), + whitelist: z.array(z.string()).optional(), + blacklist: z.array(z.string()).optional(), + options: z + .object({ + apiKey: z.string().optional(), + baseURL: z.string().optional(), + enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"), + setCacheKey: z.boolean().optional().describe("Enable promptCacheKey for this provider (default false)"), + timeout: z + .union([ + z + .number() + .int() + .positive() + .describe( + "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", + ), + z.literal(false).describe("Disable timeout for this provider entirely."), + ]) + .optional() + .describe( + "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", + ), + chunkTimeout: z + .number() + .int() + .positive() + .optional() + .describe( + "Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.", + ), + }) + .catchall(z.any()) + .optional(), + models: z.record(z.string(), Model).optional(), + }) + .partial() + .strict() + .meta({ + ref: "ProviderConfig", + }) - export type Info = z.infer -} +export type Info = z.infer + +export * as ConfigProvider from "./provider" From 1291e82bb4d881a1c0ac5cb882da2b97a169ae9d Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:00:50 -0400 Subject: [PATCH 018/335] refactor: unwrap ACP namespace + self-reexport (#22936) --- packages/opencode/src/acp/agent.ts | 3030 ++++++++++++++-------------- 1 file changed, 1515 insertions(+), 1515 deletions(-) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 5d8c723ea7..7180feabcb 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -57,793 +57,262 @@ type ModelOption = { modelId: string; name: string } const DEFAULT_VARIANT_VALUE = "default" -export namespace ACP { - const log = Log.create({ service: "acp-agent" }) +const log = Log.create({ service: "acp-agent" }) - async function getContextLimit( - sdk: OpencodeClient, - providerID: ProviderID, - modelID: ModelID, - directory: string, - ): Promise { - const providers = await sdk.config - .providers({ directory }) - .then((x) => x.data?.providers ?? []) - .catch((error) => { - log.error("failed to get providers for context limit", { error }) - return [] - }) +async function getContextLimit( + sdk: OpencodeClient, + providerID: ProviderID, + modelID: ModelID, + directory: string, +): Promise { + const providers = await sdk.config + .providers({ directory }) + .then((x) => x.data?.providers ?? []) + .catch((error) => { + log.error("failed to get providers for context limit", { error }) + return [] + }) - const provider = providers.find((p) => p.id === providerID) - const model = provider?.models[modelID] - return model?.limit.context ?? null + const provider = providers.find((p) => p.id === providerID) + const model = provider?.models[modelID] + return model?.limit.context ?? null +} + +async function sendUsageUpdate( + connection: AgentSideConnection, + sdk: OpencodeClient, + sessionID: string, + directory: string, +): Promise { + const messages = await sdk.session + .messages({ sessionID, directory }, { throwOnError: true }) + .then((x) => x.data) + .catch((error) => { + log.error("failed to fetch messages for usage update", { error }) + return undefined + }) + + if (!messages) return + + const assistantMessages = messages.filter( + (m): m is { info: AssistantMessage; parts: SessionMessageResponse["parts"] } => m.info.role === "assistant", + ) + + const lastAssistant = assistantMessages[assistantMessages.length - 1] + if (!lastAssistant) return + + const msg = lastAssistant.info + if (!msg.providerID || !msg.modelID) return + const size = await getContextLimit(sdk, ProviderID.make(msg.providerID), ModelID.make(msg.modelID), directory) + + if (!size) { + // Cannot calculate usage without known context size + return } - async function sendUsageUpdate( - connection: AgentSideConnection, - sdk: OpencodeClient, - sessionID: string, - directory: string, - ): Promise { - const messages = await sdk.session - .messages({ sessionID, directory }, { throwOnError: true }) - .then((x) => x.data) - .catch((error) => { - log.error("failed to fetch messages for usage update", { error }) - return undefined - }) + const used = msg.tokens.input + (msg.tokens.cache?.read ?? 0) + const totalCost = assistantMessages.reduce((sum, m) => sum + m.info.cost, 0) - if (!messages) return - - const assistantMessages = messages.filter( - (m): m is { info: AssistantMessage; parts: SessionMessageResponse["parts"] } => m.info.role === "assistant", - ) - - const lastAssistant = assistantMessages[assistantMessages.length - 1] - if (!lastAssistant) return - - const msg = lastAssistant.info - if (!msg.providerID || !msg.modelID) return - const size = await getContextLimit(sdk, ProviderID.make(msg.providerID), ModelID.make(msg.modelID), directory) - - if (!size) { - // Cannot calculate usage without known context size - return - } - - const used = msg.tokens.input + (msg.tokens.cache?.read ?? 0) - const totalCost = assistantMessages.reduce((sum, m) => sum + m.info.cost, 0) - - await connection - .sessionUpdate({ - sessionId: sessionID, - update: { - sessionUpdate: "usage_update", - used, - size, - cost: { amount: totalCost, currency: "USD" }, - }, - }) - .catch((error) => { - log.error("failed to send usage update", { error }) - }) - } - - export async function init({ sdk: _sdk }: { sdk: OpencodeClient }) { - return { - create: (connection: AgentSideConnection, fullConfig: ACPConfig) => { - return new Agent(connection, fullConfig) + await connection + .sessionUpdate({ + sessionId: sessionID, + update: { + sessionUpdate: "usage_update", + used, + size, + cost: { amount: totalCost, currency: "USD" }, }, - } + }) + .catch((error) => { + log.error("failed to send usage update", { error }) + }) +} + +export async function init({ sdk: _sdk }: { sdk: OpencodeClient }) { + return { + create: (connection: AgentSideConnection, fullConfig: ACPConfig) => { + return new Agent(connection, fullConfig) + }, + } +} + +export class Agent implements ACPAgent { + private connection: AgentSideConnection + private config: ACPConfig + private sdk: OpencodeClient + private sessionManager: ACPSessionManager + private eventAbort = new AbortController() + private eventStarted = false + private bashSnapshots = new Map() + private toolStarts = new Set() + private permissionQueues = new Map>() + private permissionOptions: PermissionOption[] = [ + { optionId: "once", kind: "allow_once", name: "Allow once" }, + { optionId: "always", kind: "allow_always", name: "Always allow" }, + { optionId: "reject", kind: "reject_once", name: "Reject" }, + ] + + constructor(connection: AgentSideConnection, config: ACPConfig) { + this.connection = connection + this.config = config + this.sdk = config.sdk + this.sessionManager = new ACPSessionManager(this.sdk) + this.startEventSubscription() } - export class Agent implements ACPAgent { - private connection: AgentSideConnection - private config: ACPConfig - private sdk: OpencodeClient - private sessionManager: ACPSessionManager - private eventAbort = new AbortController() - private eventStarted = false - private bashSnapshots = new Map() - private toolStarts = new Set() - private permissionQueues = new Map>() - private permissionOptions: PermissionOption[] = [ - { optionId: "once", kind: "allow_once", name: "Allow once" }, - { optionId: "always", kind: "allow_always", name: "Always allow" }, - { optionId: "reject", kind: "reject_once", name: "Reject" }, - ] + private startEventSubscription() { + if (this.eventStarted) return + this.eventStarted = true + this.runEventSubscription().catch((error) => { + if (this.eventAbort.signal.aborted) return + log.error("event subscription failed", { error }) + }) + } - constructor(connection: AgentSideConnection, config: ACPConfig) { - this.connection = connection - this.config = config - this.sdk = config.sdk - this.sessionManager = new ACPSessionManager(this.sdk) - this.startEventSubscription() - } - - private startEventSubscription() { - if (this.eventStarted) return - this.eventStarted = true - this.runEventSubscription().catch((error) => { - if (this.eventAbort.signal.aborted) return - log.error("event subscription failed", { error }) + private async runEventSubscription() { + while (true) { + if (this.eventAbort.signal.aborted) return + const events = await this.sdk.global.event({ + signal: this.eventAbort.signal, }) - } - - private async runEventSubscription() { - while (true) { + for await (const event of events.stream) { if (this.eventAbort.signal.aborted) return - const events = await this.sdk.global.event({ - signal: this.eventAbort.signal, + const payload = event?.payload + if (!payload) continue + await this.handleEvent(payload as Event).catch((error) => { + log.error("failed to handle event", { error, type: payload.type }) }) - for await (const event of events.stream) { - if (this.eventAbort.signal.aborted) return - const payload = event?.payload - if (!payload) continue - await this.handleEvent(payload as Event).catch((error) => { - log.error("failed to handle event", { error, type: payload.type }) - }) - } } } + } - private async handleEvent(event: Event) { - switch (event.type) { - case "permission.asked": { - const permission = event.properties - const session = this.sessionManager.tryGet(permission.sessionID) - if (!session) return + private async handleEvent(event: Event) { + switch (event.type) { + case "permission.asked": { + const permission = event.properties + const session = this.sessionManager.tryGet(permission.sessionID) + if (!session) return - const prev = this.permissionQueues.get(permission.sessionID) ?? Promise.resolve() - const next = prev - .then(async () => { - const directory = session.cwd + const prev = this.permissionQueues.get(permission.sessionID) ?? Promise.resolve() + const next = prev + .then(async () => { + const directory = session.cwd - const res = await this.connection - .requestPermission({ - sessionId: permission.sessionID, - toolCall: { - toolCallId: permission.tool?.callID ?? permission.id, - status: "pending", - title: permission.permission, - rawInput: permission.metadata, - kind: toToolKind(permission.permission), - locations: toLocations(permission.permission, permission.metadata), - }, - options: this.permissionOptions, + const res = await this.connection + .requestPermission({ + sessionId: permission.sessionID, + toolCall: { + toolCallId: permission.tool?.callID ?? permission.id, + status: "pending", + title: permission.permission, + rawInput: permission.metadata, + kind: toToolKind(permission.permission), + locations: toLocations(permission.permission, permission.metadata), + }, + options: this.permissionOptions, + }) + .catch(async (error) => { + log.error("failed to request permission from ACP", { + error, + permissionID: permission.id, + sessionID: permission.sessionID, }) - .catch(async (error) => { - log.error("failed to request permission from ACP", { - error, - permissionID: permission.id, - sessionID: permission.sessionID, - }) - await this.sdk.permission.reply({ - requestID: permission.id, - reply: "reject", - directory, - }) - return undefined - }) - - if (!res) return - if (res.outcome.outcome !== "selected") { await this.sdk.permission.reply({ requestID: permission.id, reply: "reject", directory, }) - return - } - - if (res.outcome.optionId !== "reject" && permission.permission == "edit") { - const metadata = permission.metadata || {} - const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : "" - const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : "" - const content = (await Filesystem.exists(filepath)) ? await Filesystem.readText(filepath) : "" - const newContent = getNewContent(content, diff) - - if (newContent) { - void this.connection.writeTextFile({ - sessionId: session.id, - path: filepath, - content: newContent, - }) - } - } + return undefined + }) + if (!res) return + if (res.outcome.outcome !== "selected") { await this.sdk.permission.reply({ requestID: permission.id, - reply: res.outcome.optionId as "once" | "always" | "reject", + reply: "reject", directory, }) - }) - .catch((error) => { - log.error("failed to handle permission", { error, permissionID: permission.id }) - }) - .finally(() => { - if (this.permissionQueues.get(permission.sessionID) === next) { - this.permissionQueues.delete(permission.sessionID) + return + } + + if (res.outcome.optionId !== "reject" && permission.permission == "edit") { + const metadata = permission.metadata || {} + const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : "" + const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : "" + const content = (await Filesystem.exists(filepath)) ? await Filesystem.readText(filepath) : "" + const newContent = getNewContent(content, diff) + + if (newContent) { + void this.connection.writeTextFile({ + sessionId: session.id, + path: filepath, + content: newContent, + }) } + } + + await this.sdk.permission.reply({ + requestID: permission.id, + reply: res.outcome.optionId as "once" | "always" | "reject", + directory, }) - this.permissionQueues.set(permission.sessionID, next) - return - } + }) + .catch((error) => { + log.error("failed to handle permission", { error, permissionID: permission.id }) + }) + .finally(() => { + if (this.permissionQueues.get(permission.sessionID) === next) { + this.permissionQueues.delete(permission.sessionID) + } + }) + this.permissionQueues.set(permission.sessionID, next) + return + } - case "message.part.updated": { - log.info("message part updated", { event: event.properties }) - const props = event.properties - const part = props.part - const session = this.sessionManager.tryGet(part.sessionID) - if (!session) return - const sessionId = session.id + case "message.part.updated": { + log.info("message part updated", { event: event.properties }) + const props = event.properties + const part = props.part + const session = this.sessionManager.tryGet(part.sessionID) + if (!session) return + const sessionId = session.id - if (part.type === "tool") { - await this.toolStart(sessionId, part) + if (part.type === "tool") { + await this.toolStart(sessionId, part) - switch (part.state.status) { - case "pending": - this.bashSnapshots.delete(part.callID) - return + switch (part.state.status) { + case "pending": + this.bashSnapshots.delete(part.callID) + return - case "running": - const output = this.bashOutput(part) - const content: ToolCallContent[] = [] - if (output) { - const hash = Hash.fast(output) - if (part.tool === "bash") { - if (this.bashSnapshots.get(part.callID) === hash) { - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.callID, - status: "in_progress", - kind: toToolKind(part.tool), - title: part.tool, - locations: toLocations(part.tool, part.state.input), - rawInput: part.state.input, - }, - }) - .catch((error) => { - log.error("failed to send tool in_progress to ACP", { error }) - }) - return - } - this.bashSnapshots.set(part.callID, hash) - } - content.push({ - type: "content", - content: { - type: "text", - text: output, - }, - }) - } - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.callID, - status: "in_progress", - kind: toToolKind(part.tool), - title: part.tool, - locations: toLocations(part.tool, part.state.input), - rawInput: part.state.input, - ...(content.length > 0 && { content }), - }, - }) - .catch((error) => { - log.error("failed to send tool in_progress to ACP", { error }) - }) - return - - case "completed": { - this.toolStarts.delete(part.callID) - this.bashSnapshots.delete(part.callID) - const kind = toToolKind(part.tool) - const content: ToolCallContent[] = [ - { - type: "content", - content: { - type: "text", - text: part.state.output, - }, - }, - ] - - if (kind === "edit") { - const input = part.state.input - const filePath = typeof input["filePath"] === "string" ? input["filePath"] : "" - const oldText = typeof input["oldString"] === "string" ? input["oldString"] : "" - const newText = - typeof input["newString"] === "string" - ? input["newString"] - : typeof input["content"] === "string" - ? input["content"] - : "" - content.push({ - type: "diff", - path: filePath, - oldText, - newText, - }) - } - - if (part.tool === "todowrite") { - const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output)) - if (parsedTodos.success) { + case "running": + const output = this.bashOutput(part) + const content: ToolCallContent[] = [] + if (output) { + const hash = Hash.fast(output) + if (part.tool === "bash") { + if (this.bashSnapshots.get(part.callID) === hash) { await this.connection .sessionUpdate({ sessionId, update: { - sessionUpdate: "plan", - entries: parsedTodos.data.map((todo) => { - const status: PlanEntry["status"] = - todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"]) - return { - priority: "medium", - status, - content: todo.content, - } - }), + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "in_progress", + kind: toToolKind(part.tool), + title: part.tool, + locations: toLocations(part.tool, part.state.input), + rawInput: part.state.input, }, }) .catch((error) => { - log.error("failed to send session update for todo", { error }) + log.error("failed to send tool in_progress to ACP", { error }) }) - } else { - log.error("failed to parse todo output", { error: parsedTodos.error }) + return } + this.bashSnapshots.set(part.callID, hash) } - - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.callID, - status: "completed", - kind, - content, - title: part.state.title, - rawInput: part.state.input, - rawOutput: { - output: part.state.output, - metadata: part.state.metadata, - }, - }, - }) - .catch((error) => { - log.error("failed to send tool completed to ACP", { error }) - }) - return - } - case "error": - this.toolStarts.delete(part.callID) - this.bashSnapshots.delete(part.callID) - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.callID, - status: "failed", - kind: toToolKind(part.tool), - title: part.tool, - rawInput: part.state.input, - content: [ - { - type: "content", - content: { - type: "text", - text: part.state.error, - }, - }, - ], - rawOutput: { - error: part.state.error, - metadata: part.state.metadata, - }, - }, - }) - .catch((error) => { - log.error("failed to send tool error to ACP", { error }) - }) - return - } - } - - // ACP clients already know the prompt they just submitted, so replaying - // live user parts duplicates the message. We still replay user history in - // loadSession() and forkSession() via processMessage(). - if (part.type !== "text" && part.type !== "file") return - - return - } - - case "message.part.delta": { - const props = event.properties - const session = this.sessionManager.tryGet(props.sessionID) - if (!session) return - const sessionId = session.id - - const message = await this.sdk.session - .message( - { - sessionID: props.sessionID, - messageID: props.messageID, - directory: session.cwd, - }, - { throwOnError: true }, - ) - .then((x) => x.data) - .catch((error) => { - log.error("unexpected error when fetching message", { error }) - return undefined - }) - - if (!message || message.info.role !== "assistant") return - - const part = message.parts.find((p) => p.id === props.partID) - if (!part) return - - if (part.type === "text" && props.field === "text" && part.ignored !== true) { - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "agent_message_chunk", - messageId: props.messageID, - content: { - type: "text", - text: props.delta, - }, - }, - }) - .catch((error) => { - log.error("failed to send text delta to ACP", { error }) - }) - return - } - - if (part.type === "reasoning" && props.field === "text") { - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "agent_thought_chunk", - messageId: props.messageID, - content: { - type: "text", - text: props.delta, - }, - }, - }) - .catch((error) => { - log.error("failed to send reasoning delta to ACP", { error }) - }) - } - return - } - } - } - - async initialize(params: InitializeRequest): Promise { - log.info("initialize", { protocolVersion: params.protocolVersion }) - - const authMethod: AuthMethod = { - description: "Run `opencode auth login` in the terminal", - name: "Login with opencode", - id: "opencode-login", - } - - // If client supports terminal-auth capability, use that instead. - if (params.clientCapabilities?._meta?.["terminal-auth"] === true) { - authMethod._meta = { - "terminal-auth": { - command: "opencode", - args: ["auth", "login"], - label: "OpenCode Login", - }, - } - } - - return { - protocolVersion: 1, - agentCapabilities: { - loadSession: true, - mcpCapabilities: { - http: true, - sse: true, - }, - promptCapabilities: { - embeddedContext: true, - image: true, - }, - sessionCapabilities: { - fork: {}, - list: {}, - resume: {}, - }, - }, - authMethods: [authMethod], - agentInfo: { - name: "OpenCode", - version: InstallationVersion, - }, - } - } - - async authenticate(_params: AuthenticateRequest) { - throw new Error("Authentication not implemented") - } - - async newSession(params: NewSessionRequest) { - const directory = params.cwd - try { - const model = await defaultModel(this.config, directory) - - // Store ACP session state - const state = await this.sessionManager.create(params.cwd, params.mcpServers, model) - const sessionId = state.id - - log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length }) - - const load = await this.loadSessionMode({ - cwd: directory, - mcpServers: params.mcpServers, - sessionId, - }) - - return { - sessionId, - configOptions: load.configOptions, - models: load.models, - modes: load.modes, - _meta: load._meta, - } - } catch (e) { - const error = MessageV2.fromError(e, { - providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), - }) - if (LoadAPIKeyError.isInstance(error)) { - throw RequestError.authRequired() - } - throw e - } - } - - async loadSession(params: LoadSessionRequest) { - const directory = params.cwd - const sessionId = params.sessionId - - try { - const model = await defaultModel(this.config, directory) - - // Store ACP session state - await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model) - - log.info("load_session", { sessionId, mcpServers: params.mcpServers.length }) - - const result = await this.loadSessionMode({ - cwd: directory, - mcpServers: params.mcpServers, - sessionId, - }) - - // Replay session history - const messages = await this.sdk.session - .messages( - { - sessionID: sessionId, - directory, - }, - { throwOnError: true }, - ) - .then((x) => x.data) - .catch((err) => { - log.error("unexpected error when fetching message", { error: err }) - return undefined - }) - - const lastUser = messages?.findLast((m) => m.info.role === "user")?.info - if (lastUser?.role === "user") { - result.models.currentModelId = `${lastUser.model.providerID}/${lastUser.model.modelID}` - this.sessionManager.setModel(sessionId, { - providerID: ProviderID.make(lastUser.model.providerID), - modelID: ModelID.make(lastUser.model.modelID), - }) - if (result.modes?.availableModes.some((m) => m.id === lastUser.agent)) { - result.modes.currentModeId = lastUser.agent - this.sessionManager.setMode(sessionId, lastUser.agent) - } - result.configOptions = buildConfigOptions({ - currentModelId: result.models.currentModelId, - availableModels: result.models.availableModels, - modes: result.modes, - }) - } - - for (const msg of messages ?? []) { - log.debug("replay message", msg) - await this.processMessage(msg) - } - - await sendUsageUpdate(this.connection, this.sdk, sessionId, directory) - - return result - } catch (e) { - const error = MessageV2.fromError(e, { - providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), - }) - if (LoadAPIKeyError.isInstance(error)) { - throw RequestError.authRequired() - } - throw e - } - } - - async listSessions(params: ListSessionsRequest): Promise { - try { - const cursor = params.cursor ? Number(params.cursor) : undefined - const limit = 100 - - const sessions = await this.sdk.session - .list( - { - directory: params.cwd ?? undefined, - roots: true, - }, - { throwOnError: true }, - ) - .then((x) => x.data ?? []) - - const sorted = sessions.toSorted((a, b) => b.time.updated - a.time.updated) - const filtered = cursor ? sorted.filter((s) => s.time.updated < cursor) : sorted - const page = filtered.slice(0, limit) - - const entries: SessionInfo[] = page.map((session) => ({ - sessionId: session.id, - cwd: session.directory, - title: session.title, - updatedAt: new Date(session.time.updated).toISOString(), - })) - - const last = page[page.length - 1] - const next = filtered.length > limit && last ? String(last.time.updated) : undefined - - const response: ListSessionsResponse = { - sessions: entries, - } - if (next) response.nextCursor = next - return response - } catch (e) { - const error = MessageV2.fromError(e, { - providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), - }) - if (LoadAPIKeyError.isInstance(error)) { - throw RequestError.authRequired() - } - throw e - } - } - - async unstable_forkSession(params: ForkSessionRequest): Promise { - const directory = params.cwd - const mcpServers = params.mcpServers ?? [] - - try { - const model = await defaultModel(this.config, directory) - - const forked = await this.sdk.session - .fork( - { - sessionID: params.sessionId, - directory, - }, - { throwOnError: true }, - ) - .then((x) => x.data) - - if (!forked) { - throw new Error("Fork session returned no data") - } - - const sessionId = forked.id - await this.sessionManager.load(sessionId, directory, mcpServers, model) - - log.info("fork_session", { sessionId, mcpServers: mcpServers.length }) - - const mode = await this.loadSessionMode({ - cwd: directory, - mcpServers, - sessionId, - }) - - const messages = await this.sdk.session - .messages( - { - sessionID: sessionId, - directory, - }, - { throwOnError: true }, - ) - .then((x) => x.data) - .catch((err) => { - log.error("unexpected error when fetching message", { error: err }) - return undefined - }) - - for (const msg of messages ?? []) { - log.debug("replay message", msg) - await this.processMessage(msg) - } - - await sendUsageUpdate(this.connection, this.sdk, sessionId, directory) - - return mode - } catch (e) { - const error = MessageV2.fromError(e, { - providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), - }) - if (LoadAPIKeyError.isInstance(error)) { - throw RequestError.authRequired() - } - throw e - } - } - - async unstable_resumeSession(params: ResumeSessionRequest): Promise { - const directory = params.cwd - const sessionId = params.sessionId - const mcpServers = params.mcpServers ?? [] - - try { - const model = await defaultModel(this.config, directory) - await this.sessionManager.load(sessionId, directory, mcpServers, model) - - log.info("resume_session", { sessionId, mcpServers: mcpServers.length }) - - const result = await this.loadSessionMode({ - cwd: directory, - mcpServers, - sessionId, - }) - - await sendUsageUpdate(this.connection, this.sdk, sessionId, directory) - - return result - } catch (e) { - const error = MessageV2.fromError(e, { - providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), - }) - if (LoadAPIKeyError.isInstance(error)) { - throw RequestError.authRequired() - } - throw e - } - } - - private async processMessage(message: SessionMessageResponse) { - log.debug("process message", message) - if (message.info.role !== "assistant" && message.info.role !== "user") return - const sessionId = message.info.sessionID - - for (const part of message.parts) { - if (part.type === "tool") { - await this.toolStart(sessionId, part) - switch (part.state.status) { - case "pending": - this.bashSnapshots.delete(part.callID) - break - case "running": - const output = this.bashOutput(part) - const runningContent: ToolCallContent[] = [] - if (output) { - runningContent.push({ + content.push({ type: "content", content: { type: "text", @@ -862,14 +331,15 @@ export namespace ACP { title: part.tool, locations: toLocations(part.tool, part.state.input), rawInput: part.state.input, - ...(runningContent.length > 0 && { content: runningContent }), + ...(content.length > 0 && { content }), }, }) - .catch((err) => { - log.error("failed to send tool in_progress to ACP", { error: err }) + .catch((error) => { + log.error("failed to send tool in_progress to ACP", { error }) }) - break - case "completed": + return + + case "completed": { this.toolStarts.delete(part.callID) this.bashSnapshots.delete(part.callID) const kind = toToolKind(part.tool) @@ -920,8 +390,8 @@ export namespace ACP { }), }, }) - .catch((err) => { - log.error("failed to send session update for todo", { error: err }) + .catch((error) => { + log.error("failed to send session update for todo", { error }) }) } else { log.error("failed to parse todo output", { error: parsedTodos.error }) @@ -945,10 +415,11 @@ export namespace ACP { }, }, }) - .catch((err) => { - log.error("failed to send tool completed to ACP", { error: err }) + .catch((error) => { + log.error("failed to send tool completed to ACP", { error }) }) - break + return + } case "error": this.toolStarts.delete(part.callID) this.bashSnapshots.delete(part.callID) @@ -977,865 +448,1394 @@ export namespace ACP { }, }, }) - .catch((err) => { - log.error("failed to send tool error to ACP", { error: err }) + .catch((error) => { + log.error("failed to send tool error to ACP", { error }) }) - break + return } - } else if (part.type === "text") { - if (part.text) { - const audience: Role[] | undefined = part.synthetic ? ["assistant"] : part.ignored ? ["user"] : undefined + } + + // ACP clients already know the prompt they just submitted, so replaying + // live user parts duplicates the message. We still replay user history in + // loadSession() and forkSession() via processMessage(). + if (part.type !== "text" && part.type !== "file") return + + return + } + + case "message.part.delta": { + const props = event.properties + const session = this.sessionManager.tryGet(props.sessionID) + if (!session) return + const sessionId = session.id + + const message = await this.sdk.session + .message( + { + sessionID: props.sessionID, + messageID: props.messageID, + directory: session.cwd, + }, + { throwOnError: true }, + ) + .then((x) => x.data) + .catch((error) => { + log.error("unexpected error when fetching message", { error }) + return undefined + }) + + if (!message || message.info.role !== "assistant") return + + const part = message.parts.find((p) => p.id === props.partID) + if (!part) return + + if (part.type === "text" && props.field === "text" && part.ignored !== true) { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "agent_message_chunk", + messageId: props.messageID, + content: { + type: "text", + text: props.delta, + }, + }, + }) + .catch((error) => { + log.error("failed to send text delta to ACP", { error }) + }) + return + } + + if (part.type === "reasoning" && props.field === "text") { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "agent_thought_chunk", + messageId: props.messageID, + content: { + type: "text", + text: props.delta, + }, + }, + }) + .catch((error) => { + log.error("failed to send reasoning delta to ACP", { error }) + }) + } + return + } + } + } + + async initialize(params: InitializeRequest): Promise { + log.info("initialize", { protocolVersion: params.protocolVersion }) + + const authMethod: AuthMethod = { + description: "Run `opencode auth login` in the terminal", + name: "Login with opencode", + id: "opencode-login", + } + + // If client supports terminal-auth capability, use that instead. + if (params.clientCapabilities?._meta?.["terminal-auth"] === true) { + authMethod._meta = { + "terminal-auth": { + command: "opencode", + args: ["auth", "login"], + label: "OpenCode Login", + }, + } + } + + return { + protocolVersion: 1, + agentCapabilities: { + loadSession: true, + mcpCapabilities: { + http: true, + sse: true, + }, + promptCapabilities: { + embeddedContext: true, + image: true, + }, + sessionCapabilities: { + fork: {}, + list: {}, + resume: {}, + }, + }, + authMethods: [authMethod], + agentInfo: { + name: "OpenCode", + version: InstallationVersion, + }, + } + } + + async authenticate(_params: AuthenticateRequest) { + throw new Error("Authentication not implemented") + } + + async newSession(params: NewSessionRequest) { + const directory = params.cwd + try { + const model = await defaultModel(this.config, directory) + + // Store ACP session state + const state = await this.sessionManager.create(params.cwd, params.mcpServers, model) + const sessionId = state.id + + log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length }) + + const load = await this.loadSessionMode({ + cwd: directory, + mcpServers: params.mcpServers, + sessionId, + }) + + return { + sessionId, + configOptions: load.configOptions, + models: load.models, + modes: load.modes, + _meta: load._meta, + } + } catch (e) { + const error = MessageV2.fromError(e, { + providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), + }) + if (LoadAPIKeyError.isInstance(error)) { + throw RequestError.authRequired() + } + throw e + } + } + + async loadSession(params: LoadSessionRequest) { + const directory = params.cwd + const sessionId = params.sessionId + + try { + const model = await defaultModel(this.config, directory) + + // Store ACP session state + await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model) + + log.info("load_session", { sessionId, mcpServers: params.mcpServers.length }) + + const result = await this.loadSessionMode({ + cwd: directory, + mcpServers: params.mcpServers, + sessionId, + }) + + // Replay session history + const messages = await this.sdk.session + .messages( + { + sessionID: sessionId, + directory, + }, + { throwOnError: true }, + ) + .then((x) => x.data) + .catch((err) => { + log.error("unexpected error when fetching message", { error: err }) + return undefined + }) + + const lastUser = messages?.findLast((m) => m.info.role === "user")?.info + if (lastUser?.role === "user") { + result.models.currentModelId = `${lastUser.model.providerID}/${lastUser.model.modelID}` + this.sessionManager.setModel(sessionId, { + providerID: ProviderID.make(lastUser.model.providerID), + modelID: ModelID.make(lastUser.model.modelID), + }) + if (result.modes?.availableModes.some((m) => m.id === lastUser.agent)) { + result.modes.currentModeId = lastUser.agent + this.sessionManager.setMode(sessionId, lastUser.agent) + } + result.configOptions = buildConfigOptions({ + currentModelId: result.models.currentModelId, + availableModels: result.models.availableModels, + modes: result.modes, + }) + } + + for (const msg of messages ?? []) { + log.debug("replay message", msg) + await this.processMessage(msg) + } + + await sendUsageUpdate(this.connection, this.sdk, sessionId, directory) + + return result + } catch (e) { + const error = MessageV2.fromError(e, { + providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), + }) + if (LoadAPIKeyError.isInstance(error)) { + throw RequestError.authRequired() + } + throw e + } + } + + async listSessions(params: ListSessionsRequest): Promise { + try { + const cursor = params.cursor ? Number(params.cursor) : undefined + const limit = 100 + + const sessions = await this.sdk.session + .list( + { + directory: params.cwd ?? undefined, + roots: true, + }, + { throwOnError: true }, + ) + .then((x) => x.data ?? []) + + const sorted = sessions.toSorted((a, b) => b.time.updated - a.time.updated) + const filtered = cursor ? sorted.filter((s) => s.time.updated < cursor) : sorted + const page = filtered.slice(0, limit) + + const entries: SessionInfo[] = page.map((session) => ({ + sessionId: session.id, + cwd: session.directory, + title: session.title, + updatedAt: new Date(session.time.updated).toISOString(), + })) + + const last = page[page.length - 1] + const next = filtered.length > limit && last ? String(last.time.updated) : undefined + + const response: ListSessionsResponse = { + sessions: entries, + } + if (next) response.nextCursor = next + return response + } catch (e) { + const error = MessageV2.fromError(e, { + providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), + }) + if (LoadAPIKeyError.isInstance(error)) { + throw RequestError.authRequired() + } + throw e + } + } + + async unstable_forkSession(params: ForkSessionRequest): Promise { + const directory = params.cwd + const mcpServers = params.mcpServers ?? [] + + try { + const model = await defaultModel(this.config, directory) + + const forked = await this.sdk.session + .fork( + { + sessionID: params.sessionId, + directory, + }, + { throwOnError: true }, + ) + .then((x) => x.data) + + if (!forked) { + throw new Error("Fork session returned no data") + } + + const sessionId = forked.id + await this.sessionManager.load(sessionId, directory, mcpServers, model) + + log.info("fork_session", { sessionId, mcpServers: mcpServers.length }) + + const mode = await this.loadSessionMode({ + cwd: directory, + mcpServers, + sessionId, + }) + + const messages = await this.sdk.session + .messages( + { + sessionID: sessionId, + directory, + }, + { throwOnError: true }, + ) + .then((x) => x.data) + .catch((err) => { + log.error("unexpected error when fetching message", { error: err }) + return undefined + }) + + for (const msg of messages ?? []) { + log.debug("replay message", msg) + await this.processMessage(msg) + } + + await sendUsageUpdate(this.connection, this.sdk, sessionId, directory) + + return mode + } catch (e) { + const error = MessageV2.fromError(e, { + providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), + }) + if (LoadAPIKeyError.isInstance(error)) { + throw RequestError.authRequired() + } + throw e + } + } + + async unstable_resumeSession(params: ResumeSessionRequest): Promise { + const directory = params.cwd + const sessionId = params.sessionId + const mcpServers = params.mcpServers ?? [] + + try { + const model = await defaultModel(this.config, directory) + await this.sessionManager.load(sessionId, directory, mcpServers, model) + + log.info("resume_session", { sessionId, mcpServers: mcpServers.length }) + + const result = await this.loadSessionMode({ + cwd: directory, + mcpServers, + sessionId, + }) + + await sendUsageUpdate(this.connection, this.sdk, sessionId, directory) + + return result + } catch (e) { + const error = MessageV2.fromError(e, { + providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), + }) + if (LoadAPIKeyError.isInstance(error)) { + throw RequestError.authRequired() + } + throw e + } + } + + private async processMessage(message: SessionMessageResponse) { + log.debug("process message", message) + if (message.info.role !== "assistant" && message.info.role !== "user") return + const sessionId = message.info.sessionID + + for (const part of message.parts) { + if (part.type === "tool") { + await this.toolStart(sessionId, part) + switch (part.state.status) { + case "pending": + this.bashSnapshots.delete(part.callID) + break + case "running": + const output = this.bashOutput(part) + const runningContent: ToolCallContent[] = [] + if (output) { + runningContent.push({ + type: "content", + content: { + type: "text", + text: output, + }, + }) + } await this.connection .sessionUpdate({ sessionId, update: { - sessionUpdate: message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk", - messageId: message.info.id, - content: { - type: "text", - text: part.text, - ...(audience && { annotations: { audience } }), + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "in_progress", + kind: toToolKind(part.tool), + title: part.tool, + locations: toLocations(part.tool, part.state.input), + rawInput: part.state.input, + ...(runningContent.length > 0 && { content: runningContent }), + }, + }) + .catch((err) => { + log.error("failed to send tool in_progress to ACP", { error: err }) + }) + break + case "completed": + this.toolStarts.delete(part.callID) + this.bashSnapshots.delete(part.callID) + const kind = toToolKind(part.tool) + const content: ToolCallContent[] = [ + { + type: "content", + content: { + type: "text", + text: part.state.output, + }, + }, + ] + + if (kind === "edit") { + const input = part.state.input + const filePath = typeof input["filePath"] === "string" ? input["filePath"] : "" + const oldText = typeof input["oldString"] === "string" ? input["oldString"] : "" + const newText = + typeof input["newString"] === "string" + ? input["newString"] + : typeof input["content"] === "string" + ? input["content"] + : "" + content.push({ + type: "diff", + path: filePath, + oldText, + newText, + }) + } + + if (part.tool === "todowrite") { + const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output)) + if (parsedTodos.success) { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "plan", + entries: parsedTodos.data.map((todo) => { + const status: PlanEntry["status"] = + todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"]) + return { + priority: "medium", + status, + content: todo.content, + } + }), + }, + }) + .catch((err) => { + log.error("failed to send session update for todo", { error: err }) + }) + } else { + log.error("failed to parse todo output", { error: parsedTodos.error }) + } + } + + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "completed", + kind, + content, + title: part.state.title, + rawInput: part.state.input, + rawOutput: { + output: part.state.output, + metadata: part.state.metadata, }, }, }) .catch((err) => { - log.error("failed to send text to ACP", { error: err }) + log.error("failed to send tool completed to ACP", { error: err }) }) - } - } else if (part.type === "file") { - // Replay file attachments as appropriate ACP content blocks. - // OpenCode stores files internally as { type: "file", url, filename, mime }. - // We convert these back to ACP blocks based on the URL scheme and MIME type: - // - file:// URLs → resource_link - // - data: URLs with image/* → image block - // - data: URLs with text/* or application/json → resource with text - // - data: URLs with other types → resource with blob - const url = part.url - const filename = part.filename ?? "file" - const mime = part.mime || "application/octet-stream" - const messageChunk = message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk" + break + case "error": + this.toolStarts.delete(part.callID) + this.bashSnapshots.delete(part.callID) + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: part.callID, + status: "failed", + kind: toToolKind(part.tool), + title: part.tool, + rawInput: part.state.input, + content: [ + { + type: "content", + content: { + type: "text", + text: part.state.error, + }, + }, + ], + rawOutput: { + error: part.state.error, + metadata: part.state.metadata, + }, + }, + }) + .catch((err) => { + log.error("failed to send tool error to ACP", { error: err }) + }) + break + } + } else if (part.type === "text") { + if (part.text) { + const audience: Role[] | undefined = part.synthetic ? ["assistant"] : part.ignored ? ["user"] : undefined + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk", + messageId: message.info.id, + content: { + type: "text", + text: part.text, + ...(audience && { annotations: { audience } }), + }, + }, + }) + .catch((err) => { + log.error("failed to send text to ACP", { error: err }) + }) + } + } else if (part.type === "file") { + // Replay file attachments as appropriate ACP content blocks. + // OpenCode stores files internally as { type: "file", url, filename, mime }. + // We convert these back to ACP blocks based on the URL scheme and MIME type: + // - file:// URLs → resource_link + // - data: URLs with image/* → image block + // - data: URLs with text/* or application/json → resource with text + // - data: URLs with other types → resource with blob + const url = part.url + const filename = part.filename ?? "file" + const mime = part.mime || "application/octet-stream" + const messageChunk = message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk" - if (url.startsWith("file://")) { - // Local file reference - send as resource_link + if (url.startsWith("file://")) { + // Local file reference - send as resource_link + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: messageChunk, + messageId: message.info.id, + content: { type: "resource_link", uri: url, name: filename, mimeType: mime }, + }, + }) + .catch((err) => { + log.error("failed to send resource_link to ACP", { error: err }) + }) + } else if (url.startsWith("data:")) { + // Embedded content - parse data URL and send as appropriate block type + const base64Match = url.match(/^data:([^;]+);base64,(.*)$/) + const dataMime = base64Match?.[1] + const base64Data = base64Match?.[2] ?? "" + + const effectiveMime = dataMime || mime + + if (effectiveMime.startsWith("image/")) { + // Image - send as image block await this.connection .sessionUpdate({ sessionId, update: { sessionUpdate: messageChunk, messageId: message.info.id, - content: { type: "resource_link", uri: url, name: filename, mimeType: mime }, + content: { + type: "image", + mimeType: effectiveMime, + data: base64Data, + uri: pathToFileURL(filename).href, + }, }, }) .catch((err) => { - log.error("failed to send resource_link to ACP", { error: err }) + log.error("failed to send image to ACP", { error: err }) }) - } else if (url.startsWith("data:")) { - // Embedded content - parse data URL and send as appropriate block type - const base64Match = url.match(/^data:([^;]+);base64,(.*)$/) - const dataMime = base64Match?.[1] - const base64Data = base64Match?.[2] ?? "" + } else { + // Non-image: text types get decoded, binary types stay as blob + const isText = effectiveMime.startsWith("text/") || effectiveMime === "application/json" + const fileUri = pathToFileURL(filename).href + const resource = isText + ? { + uri: fileUri, + mimeType: effectiveMime, + text: Buffer.from(base64Data, "base64").toString("utf-8"), + } + : { uri: fileUri, mimeType: effectiveMime, blob: base64Data } - const effectiveMime = dataMime || mime - - if (effectiveMime.startsWith("image/")) { - // Image - send as image block - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: messageChunk, - messageId: message.info.id, - content: { - type: "image", - mimeType: effectiveMime, - data: base64Data, - uri: pathToFileURL(filename).href, - }, - }, - }) - .catch((err) => { - log.error("failed to send image to ACP", { error: err }) - }) - } else { - // Non-image: text types get decoded, binary types stay as blob - const isText = effectiveMime.startsWith("text/") || effectiveMime === "application/json" - const fileUri = pathToFileURL(filename).href - const resource = isText - ? { - uri: fileUri, - mimeType: effectiveMime, - text: Buffer.from(base64Data, "base64").toString("utf-8"), - } - : { uri: fileUri, mimeType: effectiveMime, blob: base64Data } - - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: messageChunk, - messageId: message.info.id, - content: { type: "resource", resource }, - }, - }) - .catch((err) => { - log.error("failed to send resource to ACP", { error: err }) - }) - } - } - // URLs that don't match file:// or data: are skipped (unsupported) - } else if (part.type === "reasoning") { - if (part.text) { await this.connection .sessionUpdate({ sessionId, update: { - sessionUpdate: "agent_thought_chunk", + sessionUpdate: messageChunk, messageId: message.info.id, - content: { - type: "text", - text: part.text, - }, + content: { type: "resource", resource }, }, }) .catch((err) => { - log.error("failed to send reasoning to ACP", { error: err }) + log.error("failed to send resource to ACP", { error: err }) }) } } - } - } - - private bashOutput(part: ToolPart) { - if (part.tool !== "bash") return - if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return - const output = part.state.metadata["output"] - if (typeof output !== "string") return - return output - } - - private async toolStart(sessionId: string, part: ToolPart) { - if (this.toolStarts.has(part.callID)) return - this.toolStarts.add(part.callID) - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call", - toolCallId: part.callID, - title: part.tool, - kind: toToolKind(part.tool), - status: "pending", - locations: [], - rawInput: {}, - }, - }) - .catch((error) => { - log.error("failed to send tool pending to ACP", { error }) - }) - } - - private async loadAvailableModes(directory: string): Promise { - const agents = await this.config.sdk.app - .agents( - { - directory, - }, - { throwOnError: true }, - ) - .then((resp) => resp.data!) - - return agents - .filter((agent) => agent.mode !== "subagent" && !agent.hidden) - .map((agent) => ({ - id: agent.name, - name: agent.name, - description: agent.description, - })) - } - - private async resolveModeState( - directory: string, - sessionId: string, - ): Promise<{ availableModes: ModeOption[]; currentModeId?: string }> { - const availableModes = await this.loadAvailableModes(directory) - const currentModeId = - this.sessionManager.get(sessionId).modeId || - (await (async () => { - if (!availableModes.length) return undefined - const defaultAgentName = await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent())) - const resolvedModeId = - availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id - this.sessionManager.setMode(sessionId, resolvedModeId) - return resolvedModeId - })()) - - return { availableModes, currentModeId } - } - - private async loadSessionMode(params: LoadSessionRequest) { - const directory = params.cwd - const model = await defaultModel(this.config, directory) - const sessionId = params.sessionId - - const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers) - const entries = sortProvidersByName(providers) - const availableVariants = modelVariantsFromProviders(entries, model) - const currentVariant = this.sessionManager.getVariant(sessionId) - if (currentVariant && !availableVariants.includes(currentVariant)) { - this.sessionManager.setVariant(sessionId, undefined) - } - const availableModels = buildAvailableModels(entries, { includeVariants: true }) - const modeState = await this.resolveModeState(directory, sessionId) - const currentModeId = modeState.currentModeId - const modes = currentModeId - ? { - availableModes: modeState.availableModes, - currentModeId, - } - : undefined - - const commands = await this.config.sdk.command - .list( - { - directory, - }, - { throwOnError: true }, - ) - .then((resp) => resp.data!) - - const availableCommands = commands.map((command) => ({ - name: command.name, - description: command.description ?? "", - })) - const names = new Set(availableCommands.map((c) => c.name)) - if (!names.has("compact")) - availableCommands.push({ - name: "compact", - description: "compact the session", - }) - - const mcpServers: Record = {} - for (const server of params.mcpServers) { - if ("type" in server) { - mcpServers[server.name] = { - url: server.url, - headers: server.headers.reduce>((acc, { name, value }) => { - acc[name] = value - return acc - }, {}), - type: "remote", - } - } else { - mcpServers[server.name] = { - type: "local", - command: [server.command, ...server.args], - environment: server.env.reduce>((acc, { name, value }) => { - acc[name] = value - return acc - }, {}), - } - } - } - - await Promise.all( - Object.entries(mcpServers).map(async ([key, mcp]) => { - await this.sdk.mcp - .add( - { - directory, - name: key, - config: mcp, + // URLs that don't match file:// or data: are skipped (unsupported) + } else if (part.type === "reasoning") { + if (part.text) { + await this.connection + .sessionUpdate({ + sessionId, + update: { + sessionUpdate: "agent_thought_chunk", + messageId: message.info.id, + content: { + type: "text", + text: part.text, + }, }, - { throwOnError: true }, - ) - .catch((error) => { - log.error("failed to add mcp server", { name: key, error }) }) - }), - ) + .catch((err) => { + log.error("failed to send reasoning to ACP", { error: err }) + }) + } + } + } + } - setTimeout(() => { - void this.connection.sessionUpdate({ - sessionId, - update: { - sessionUpdate: "available_commands_update", - availableCommands, - }, - }) - }, 0) + private bashOutput(part: ToolPart) { + if (part.tool !== "bash") return + if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return + const output = part.state.metadata["output"] + if (typeof output !== "string") return + return output + } - return { + private async toolStart(sessionId: string, part: ToolPart) { + if (this.toolStarts.has(part.callID)) return + this.toolStarts.add(part.callID) + await this.connection + .sessionUpdate({ sessionId, - models: { - currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true), - availableModels, + update: { + sessionUpdate: "tool_call", + toolCallId: part.callID, + title: part.tool, + kind: toToolKind(part.tool), + status: "pending", + locations: [], + rawInput: {}, }, - modes, - configOptions: buildConfigOptions({ - currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true), - availableModels, - modes, - }), - _meta: buildVariantMeta({ - model, - variant: this.sessionManager.getVariant(sessionId), - availableVariants, - }), - } + }) + .catch((error) => { + log.error("failed to send tool pending to ACP", { error }) + }) + } + + private async loadAvailableModes(directory: string): Promise { + const agents = await this.config.sdk.app + .agents( + { + directory, + }, + { throwOnError: true }, + ) + .then((resp) => resp.data!) + + return agents + .filter((agent) => agent.mode !== "subagent" && !agent.hidden) + .map((agent) => ({ + id: agent.name, + name: agent.name, + description: agent.description, + })) + } + + private async resolveModeState( + directory: string, + sessionId: string, + ): Promise<{ availableModes: ModeOption[]; currentModeId?: string }> { + const availableModes = await this.loadAvailableModes(directory) + const currentModeId = + this.sessionManager.get(sessionId).modeId || + (await (async () => { + if (!availableModes.length) return undefined + const defaultAgentName = await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent())) + const resolvedModeId = + availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id + this.sessionManager.setMode(sessionId, resolvedModeId) + return resolvedModeId + })()) + + return { availableModes, currentModeId } + } + + private async loadSessionMode(params: LoadSessionRequest) { + const directory = params.cwd + const model = await defaultModel(this.config, directory) + const sessionId = params.sessionId + + const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers) + const entries = sortProvidersByName(providers) + const availableVariants = modelVariantsFromProviders(entries, model) + const currentVariant = this.sessionManager.getVariant(sessionId) + if (currentVariant && !availableVariants.includes(currentVariant)) { + this.sessionManager.setVariant(sessionId, undefined) } - - async unstable_setSessionModel(params: SetSessionModelRequest) { - const session = this.sessionManager.get(params.sessionId) - const providers = await this.sdk.config - .providers({ directory: session.cwd }, { throwOnError: true }) - .then((x) => x.data!.providers) - - const selection = parseModelSelection(params.modelId, providers) - this.sessionManager.setModel(session.id, selection.model) - this.sessionManager.setVariant(session.id, selection.variant) - - const entries = sortProvidersByName(providers) - const availableVariants = modelVariantsFromProviders(entries, selection.model) - - return { - _meta: buildVariantMeta({ - model: selection.model, - variant: selection.variant, - availableVariants, - }), - } - } - - async setSessionMode(params: SetSessionModeRequest): Promise { - const session = this.sessionManager.get(params.sessionId) - const availableModes = await this.loadAvailableModes(session.cwd) - if (!availableModes.some((mode) => mode.id === params.modeId)) { - throw new Error(`Agent not found: ${params.modeId}`) - } - this.sessionManager.setMode(params.sessionId, params.modeId) - } - - async setSessionConfigOption(params: SetSessionConfigOptionRequest): Promise { - const session = this.sessionManager.get(params.sessionId) - const providers = await this.sdk.config - .providers({ directory: session.cwd }, { throwOnError: true }) - .then((x) => x.data!.providers) - const entries = sortProvidersByName(providers) - - if (params.configId === "model") { - if (typeof params.value !== "string") throw RequestError.invalidParams("model value must be a string") - const selection = parseModelSelection(params.value, providers) - this.sessionManager.setModel(session.id, selection.model) - this.sessionManager.setVariant(session.id, selection.variant) - } else if (params.configId === "mode") { - if (typeof params.value !== "string") throw RequestError.invalidParams("mode value must be a string") - const availableModes = await this.loadAvailableModes(session.cwd) - if (!availableModes.some((mode) => mode.id === params.value)) { - throw RequestError.invalidParams(JSON.stringify({ error: `Mode not found: ${params.value}` })) + const availableModels = buildAvailableModels(entries, { includeVariants: true }) + const modeState = await this.resolveModeState(directory, sessionId) + const currentModeId = modeState.currentModeId + const modes = currentModeId + ? { + availableModes: modeState.availableModes, + currentModeId, } - this.sessionManager.setMode(session.id, params.value) - } else { - throw RequestError.invalidParams(JSON.stringify({ error: `Unknown config option: ${params.configId}` })) - } + : undefined - const updatedSession = this.sessionManager.get(session.id) - const model = updatedSession.model ?? (await defaultModel(this.config, session.cwd)) - const availableVariants = modelVariantsFromProviders(entries, model) - const currentModelId = formatModelIdWithVariant(model, updatedSession.variant, availableVariants, true) - const availableModels = buildAvailableModels(entries, { includeVariants: true }) - const modeState = await this.resolveModeState(session.cwd, session.id) - const modes = modeState.currentModeId - ? { availableModes: modeState.availableModes, currentModeId: modeState.currentModeId } - : undefined + const commands = await this.config.sdk.command + .list( + { + directory, + }, + { throwOnError: true }, + ) + .then((resp) => resp.data!) - return { - configOptions: buildConfigOptions({ currentModelId, availableModels, modes }), - } - } - - async prompt(params: PromptRequest) { - const sessionID = params.sessionId - const session = this.sessionManager.get(sessionID) - const directory = session.cwd - - const current = session.model - const model = current ?? (await defaultModel(this.config, directory)) - if (!current) { - this.sessionManager.setModel(session.id, model) - } - const agent = - session.modeId ?? (await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent()))) - - const parts: Array< - | { type: "text"; text: string; synthetic?: boolean; ignored?: boolean } - | { type: "file"; url: string; filename: string; mime: string } - > = [] - for (const part of params.prompt) { - switch (part.type) { - case "text": - const audience = part.annotations?.audience - const forAssistant = audience?.length === 1 && audience[0] === "assistant" - const forUser = audience?.length === 1 && audience[0] === "user" - parts.push({ - type: "text" as const, - text: part.text, - ...(forAssistant && { synthetic: true }), - ...(forUser && { ignored: true }), - }) - break - case "image": { - const parsed = parseUri(part.uri ?? "") - const filename = parsed.type === "file" ? parsed.filename : "image" - if (part.data) { - parts.push({ - type: "file", - url: `data:${part.mimeType};base64,${part.data}`, - filename, - mime: part.mimeType, - }) - } else if (part.uri && part.uri.startsWith("http:")) { - parts.push({ - type: "file", - url: part.uri, - filename, - mime: part.mimeType, - }) - } - break - } - - case "resource_link": - const parsed = parseUri(part.uri) - // Use the name from resource_link if available - if (part.name && parsed.type === "file") { - parsed.filename = part.name - } - parts.push(parsed) - - break - - case "resource": { - const resource = part.resource - if ("text" in resource && resource.text) { - parts.push({ - type: "text", - text: resource.text, - }) - } else if ("blob" in resource && resource.blob && resource.mimeType) { - // Binary resource (PDFs, etc.): store as file part with data URL - const parsed = parseUri(resource.uri ?? "") - const filename = parsed.type === "file" ? parsed.filename : "file" - parts.push({ - type: "file", - url: `data:${resource.mimeType};base64,${resource.blob}`, - filename, - mime: resource.mimeType, - }) - } - break - } - - default: - break - } - } - - log.info("parts", { parts }) - - const cmd = (() => { - const text = parts - .filter((p): p is { type: "text"; text: string } => p.type === "text") - .map((p) => p.text) - .join("") - .trim() - - if (!text.startsWith("/")) return - - const [name, ...rest] = text.slice(1).split(/\s+/) - return { name, args: rest.join(" ").trim() } - })() - - const buildUsage = (msg: AssistantMessage): Usage => ({ - totalTokens: - msg.tokens.input + - msg.tokens.output + - msg.tokens.reasoning + - (msg.tokens.cache?.read ?? 0) + - (msg.tokens.cache?.write ?? 0), - inputTokens: msg.tokens.input, - outputTokens: msg.tokens.output, - thoughtTokens: msg.tokens.reasoning || undefined, - cachedReadTokens: msg.tokens.cache?.read || undefined, - cachedWriteTokens: msg.tokens.cache?.write || undefined, + const availableCommands = commands.map((command) => ({ + name: command.name, + description: command.description ?? "", + })) + const names = new Set(availableCommands.map((c) => c.name)) + if (!names.has("compact")) + availableCommands.push({ + name: "compact", + description: "compact the session", }) - if (!cmd) { - const response = await this.sdk.session.prompt({ - sessionID, - model: { - providerID: model.providerID, - modelID: model.modelID, - }, - variant: this.sessionManager.getVariant(sessionID), - parts, - agent, - directory, - }) - const msg = response.data?.info - - await sendUsageUpdate(this.connection, this.sdk, sessionID, directory) - - return { - stopReason: "end_turn" as const, - usage: msg ? buildUsage(msg) : undefined, - _meta: {}, + const mcpServers: Record = {} + for (const server of params.mcpServers) { + if ("type" in server) { + mcpServers[server.name] = { + url: server.url, + headers: server.headers.reduce>((acc, { name, value }) => { + acc[name] = value + return acc + }, {}), + type: "remote", + } + } else { + mcpServers[server.name] = { + type: "local", + command: [server.command, ...server.args], + environment: server.env.reduce>((acc, { name, value }) => { + acc[name] = value + return acc + }, {}), } } + } - const command = await this.config.sdk.command - .list({ directory }, { throwOnError: true }) - .then((x) => x.data!.find((c) => c.name === cmd.name)) - if (command) { - const response = await this.sdk.session.command({ - sessionID, - command: command.name, - arguments: cmd.args, - model: model.providerID + "/" + model.modelID, - agent, - directory, - }) - const msg = response.data?.info - - await sendUsageUpdate(this.connection, this.sdk, sessionID, directory) - - return { - stopReason: "end_turn" as const, - usage: msg ? buildUsage(msg) : undefined, - _meta: {}, - } - } - - switch (cmd.name) { - case "compact": - await this.config.sdk.session.summarize( + await Promise.all( + Object.entries(mcpServers).map(async ([key, mcp]) => { + await this.sdk.mcp + .add( { - sessionID, directory, - providerID: model.providerID, - modelID: model.modelID, + name: key, + config: mcp, }, { throwOnError: true }, ) + .catch((error) => { + log.error("failed to add mcp server", { name: key, error }) + }) + }), + ) + + setTimeout(() => { + void this.connection.sessionUpdate({ + sessionId, + update: { + sessionUpdate: "available_commands_update", + availableCommands, + }, + }) + }, 0) + + return { + sessionId, + models: { + currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true), + availableModels, + }, + modes, + configOptions: buildConfigOptions({ + currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true), + availableModels, + modes, + }), + _meta: buildVariantMeta({ + model, + variant: this.sessionManager.getVariant(sessionId), + availableVariants, + }), + } + } + + async unstable_setSessionModel(params: SetSessionModelRequest) { + const session = this.sessionManager.get(params.sessionId) + const providers = await this.sdk.config + .providers({ directory: session.cwd }, { throwOnError: true }) + .then((x) => x.data!.providers) + + const selection = parseModelSelection(params.modelId, providers) + this.sessionManager.setModel(session.id, selection.model) + this.sessionManager.setVariant(session.id, selection.variant) + + const entries = sortProvidersByName(providers) + const availableVariants = modelVariantsFromProviders(entries, selection.model) + + return { + _meta: buildVariantMeta({ + model: selection.model, + variant: selection.variant, + availableVariants, + }), + } + } + + async setSessionMode(params: SetSessionModeRequest): Promise { + const session = this.sessionManager.get(params.sessionId) + const availableModes = await this.loadAvailableModes(session.cwd) + if (!availableModes.some((mode) => mode.id === params.modeId)) { + throw new Error(`Agent not found: ${params.modeId}`) + } + this.sessionManager.setMode(params.sessionId, params.modeId) + } + + async setSessionConfigOption(params: SetSessionConfigOptionRequest): Promise { + const session = this.sessionManager.get(params.sessionId) + const providers = await this.sdk.config + .providers({ directory: session.cwd }, { throwOnError: true }) + .then((x) => x.data!.providers) + const entries = sortProvidersByName(providers) + + if (params.configId === "model") { + if (typeof params.value !== "string") throw RequestError.invalidParams("model value must be a string") + const selection = parseModelSelection(params.value, providers) + this.sessionManager.setModel(session.id, selection.model) + this.sessionManager.setVariant(session.id, selection.variant) + } else if (params.configId === "mode") { + if (typeof params.value !== "string") throw RequestError.invalidParams("mode value must be a string") + const availableModes = await this.loadAvailableModes(session.cwd) + if (!availableModes.some((mode) => mode.id === params.value)) { + throw RequestError.invalidParams(JSON.stringify({ error: `Mode not found: ${params.value}` })) + } + this.sessionManager.setMode(session.id, params.value) + } else { + throw RequestError.invalidParams(JSON.stringify({ error: `Unknown config option: ${params.configId}` })) + } + + const updatedSession = this.sessionManager.get(session.id) + const model = updatedSession.model ?? (await defaultModel(this.config, session.cwd)) + const availableVariants = modelVariantsFromProviders(entries, model) + const currentModelId = formatModelIdWithVariant(model, updatedSession.variant, availableVariants, true) + const availableModels = buildAvailableModels(entries, { includeVariants: true }) + const modeState = await this.resolveModeState(session.cwd, session.id) + const modes = modeState.currentModeId + ? { availableModes: modeState.availableModes, currentModeId: modeState.currentModeId } + : undefined + + return { + configOptions: buildConfigOptions({ currentModelId, availableModels, modes }), + } + } + + async prompt(params: PromptRequest) { + const sessionID = params.sessionId + const session = this.sessionManager.get(sessionID) + const directory = session.cwd + + const current = session.model + const model = current ?? (await defaultModel(this.config, directory)) + if (!current) { + this.sessionManager.setModel(session.id, model) + } + const agent = + session.modeId ?? (await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent()))) + + const parts: Array< + | { type: "text"; text: string; synthetic?: boolean; ignored?: boolean } + | { type: "file"; url: string; filename: string; mime: string } + > = [] + for (const part of params.prompt) { + switch (part.type) { + case "text": + const audience = part.annotations?.audience + const forAssistant = audience?.length === 1 && audience[0] === "assistant" + const forUser = audience?.length === 1 && audience[0] === "user" + parts.push({ + type: "text" as const, + text: part.text, + ...(forAssistant && { synthetic: true }), + ...(forUser && { ignored: true }), + }) + break + case "image": { + const parsed = parseUri(part.uri ?? "") + const filename = parsed.type === "file" ? parsed.filename : "image" + if (part.data) { + parts.push({ + type: "file", + url: `data:${part.mimeType};base64,${part.data}`, + filename, + mime: part.mimeType, + }) + } else if (part.uri && part.uri.startsWith("http:")) { + parts.push({ + type: "file", + url: part.uri, + filename, + mime: part.mimeType, + }) + } + break + } + + case "resource_link": + const parsed = parseUri(part.uri) + // Use the name from resource_link if available + if (part.name && parsed.type === "file") { + parsed.filename = part.name + } + parts.push(parsed) + + break + + case "resource": { + const resource = part.resource + if ("text" in resource && resource.text) { + parts.push({ + type: "text", + text: resource.text, + }) + } else if ("blob" in resource && resource.blob && resource.mimeType) { + // Binary resource (PDFs, etc.): store as file part with data URL + const parsed = parseUri(resource.uri ?? "") + const filename = parsed.type === "file" ? parsed.filename : "file" + parts.push({ + type: "file", + url: `data:${resource.mimeType};base64,${resource.blob}`, + filename, + mime: resource.mimeType, + }) + } + break + } + + default: break } + } + + log.info("parts", { parts }) + + const cmd = (() => { + const text = parts + .filter((p): p is { type: "text"; text: string } => p.type === "text") + .map((p) => p.text) + .join("") + .trim() + + if (!text.startsWith("/")) return + + const [name, ...rest] = text.slice(1).split(/\s+/) + return { name, args: rest.join(" ").trim() } + })() + + const buildUsage = (msg: AssistantMessage): Usage => ({ + totalTokens: + msg.tokens.input + + msg.tokens.output + + msg.tokens.reasoning + + (msg.tokens.cache?.read ?? 0) + + (msg.tokens.cache?.write ?? 0), + inputTokens: msg.tokens.input, + outputTokens: msg.tokens.output, + thoughtTokens: msg.tokens.reasoning || undefined, + cachedReadTokens: msg.tokens.cache?.read || undefined, + cachedWriteTokens: msg.tokens.cache?.write || undefined, + }) + + if (!cmd) { + const response = await this.sdk.session.prompt({ + sessionID, + model: { + providerID: model.providerID, + modelID: model.modelID, + }, + variant: this.sessionManager.getVariant(sessionID), + parts, + agent, + directory, + }) + const msg = response.data?.info await sendUsageUpdate(this.connection, this.sdk, sessionID, directory) return { stopReason: "end_turn" as const, + usage: msg ? buildUsage(msg) : undefined, _meta: {}, } } - async cancel(params: CancelNotification) { - const session = this.sessionManager.get(params.sessionId) - await this.config.sdk.session.abort( - { - sessionID: params.sessionId, - directory: session.cwd, - }, - { throwOnError: true }, - ) - } - } - - function toToolKind(toolName: string): ToolKind { - const tool = toolName.toLocaleLowerCase() - switch (tool) { - case "bash": - return "execute" - case "webfetch": - return "fetch" - - case "edit": - case "patch": - case "write": - return "edit" - - case "grep": - case "glob": - case "context7_resolve_library_id": - case "context7_get_library_docs": - return "search" - - case "read": - return "read" - - default: - return "other" - } - } - - function toLocations(toolName: string, input: Record): { path: string }[] { - const tool = toolName.toLocaleLowerCase() - switch (tool) { - case "read": - case "edit": - case "write": - return input["filePath"] ? [{ path: input["filePath"] }] : [] - case "glob": - case "grep": - return input["path"] ? [{ path: input["path"] }] : [] - case "bash": - return [] - default: - return [] - } - } - - async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ providerID: ProviderID; modelID: ModelID }> { - const sdk = config.sdk - const configured = config.defaultModel - if (configured) return configured - - const directory = cwd ?? process.cwd() - - const specified = await sdk.config - .get({ directory }, { throwOnError: true }) - .then((resp) => { - const cfg = resp.data - if (!cfg || !cfg.model) return undefined - return Provider.parseModel(cfg.model) - }) - .catch((error) => { - log.error("failed to load user config for default model", { error }) - return undefined + const command = await this.config.sdk.command + .list({ directory }, { throwOnError: true }) + .then((x) => x.data!.find((c) => c.name === cmd.name)) + if (command) { + const response = await this.sdk.session.command({ + sessionID, + command: command.name, + arguments: cmd.args, + model: model.providerID + "/" + model.modelID, + agent, + directory, }) + const msg = response.data?.info - const providers = await sdk.config - .providers({ directory }, { throwOnError: true }) - .then((x) => x.data?.providers ?? []) - .catch((error) => { - log.error("failed to list providers for default model", { error }) - return [] - }) + await sendUsageUpdate(this.connection, this.sdk, sessionID, directory) - if (specified && providers.length) { - const provider = providers.find((p) => p.id === specified.providerID) - if (provider && provider.models[specified.modelID]) return specified - } - - if (specified && !providers.length) return specified - - const opencodeProvider = providers.find((p) => p.id === "opencode") - if (opencodeProvider) { - if (opencodeProvider.models["big-pickle"]) { - return { providerID: ProviderID.opencode, modelID: ModelID.make("big-pickle") } - } - const [best] = Provider.sort(Object.values(opencodeProvider.models)) - if (best) { - return { - providerID: ProviderID.make(best.providerID), - modelID: ModelID.make(best.id), - } + return { + stopReason: "end_turn" as const, + usage: msg ? buildUsage(msg) : undefined, + _meta: {}, } } - const models = providers.flatMap((p) => Object.values(p.models)) - const [best] = Provider.sort(models) + switch (cmd.name) { + case "compact": + await this.config.sdk.session.summarize( + { + sessionID, + directory, + providerID: model.providerID, + modelID: model.modelID, + }, + { throwOnError: true }, + ) + break + } + + await sendUsageUpdate(this.connection, this.sdk, sessionID, directory) + + return { + stopReason: "end_turn" as const, + _meta: {}, + } + } + + async cancel(params: CancelNotification) { + const session = this.sessionManager.get(params.sessionId) + await this.config.sdk.session.abort( + { + sessionID: params.sessionId, + directory: session.cwd, + }, + { throwOnError: true }, + ) + } +} + +function toToolKind(toolName: string): ToolKind { + const tool = toolName.toLocaleLowerCase() + switch (tool) { + case "bash": + return "execute" + case "webfetch": + return "fetch" + + case "edit": + case "patch": + case "write": + return "edit" + + case "grep": + case "glob": + case "context7_resolve_library_id": + case "context7_get_library_docs": + return "search" + + case "read": + return "read" + + default: + return "other" + } +} + +function toLocations(toolName: string, input: Record): { path: string }[] { + const tool = toolName.toLocaleLowerCase() + switch (tool) { + case "read": + case "edit": + case "write": + return input["filePath"] ? [{ path: input["filePath"] }] : [] + case "glob": + case "grep": + return input["path"] ? [{ path: input["path"] }] : [] + case "bash": + return [] + default: + return [] + } +} + +async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ providerID: ProviderID; modelID: ModelID }> { + const sdk = config.sdk + const configured = config.defaultModel + if (configured) return configured + + const directory = cwd ?? process.cwd() + + const specified = await sdk.config + .get({ directory }, { throwOnError: true }) + .then((resp) => { + const cfg = resp.data + if (!cfg || !cfg.model) return undefined + return Provider.parseModel(cfg.model) + }) + .catch((error) => { + log.error("failed to load user config for default model", { error }) + return undefined + }) + + const providers = await sdk.config + .providers({ directory }, { throwOnError: true }) + .then((x) => x.data?.providers ?? []) + .catch((error) => { + log.error("failed to list providers for default model", { error }) + return [] + }) + + if (specified && providers.length) { + const provider = providers.find((p) => p.id === specified.providerID) + if (provider && provider.models[specified.modelID]) return specified + } + + if (specified && !providers.length) return specified + + const opencodeProvider = providers.find((p) => p.id === "opencode") + if (opencodeProvider) { + if (opencodeProvider.models["big-pickle"]) { + return { providerID: ProviderID.opencode, modelID: ModelID.make("big-pickle") } + } + const [best] = Provider.sort(Object.values(opencodeProvider.models)) if (best) { return { providerID: ProviderID.make(best.providerID), modelID: ModelID.make(best.id), } } - - if (specified) return specified - - return { providerID: ProviderID.opencode, modelID: ModelID.make("big-pickle") } } - function parseUri( - uri: string, - ): { type: "file"; url: string; filename: string; mime: string } | { type: "text"; text: string } { - try { - if (uri.startsWith("file://")) { - const path = uri.slice(7) + const models = providers.flatMap((p) => Object.values(p.models)) + const [best] = Provider.sort(models) + if (best) { + return { + providerID: ProviderID.make(best.providerID), + modelID: ModelID.make(best.id), + } + } + + if (specified) return specified + + return { providerID: ProviderID.opencode, modelID: ModelID.make("big-pickle") } +} + +function parseUri( + uri: string, +): { type: "file"; url: string; filename: string; mime: string } | { type: "text"; text: string } { + try { + if (uri.startsWith("file://")) { + const path = uri.slice(7) + const name = path.split("/").pop() || path + return { + type: "file", + url: uri, + filename: name, + mime: "text/plain", + } + } + if (uri.startsWith("zed://")) { + const url = new URL(uri) + const path = url.searchParams.get("path") + if (path) { const name = path.split("/").pop() || path return { type: "file", - url: uri, + url: pathToFileURL(path).href, filename: name, mime: "text/plain", } } - if (uri.startsWith("zed://")) { - const url = new URL(uri) - const path = url.searchParams.get("path") - if (path) { - const name = path.split("/").pop() || path - return { - type: "file", - url: pathToFileURL(path).href, - filename: name, - mime: "text/plain", - } - } - } - return { - type: "text", - text: uri, - } - } catch { - return { - type: "text", - text: uri, - } } - } - - function getNewContent(fileOriginal: string, unifiedDiff: string): string | undefined { - const result = applyPatch(fileOriginal, unifiedDiff) - if (result === false) { - log.error("Failed to apply unified diff (context mismatch)") - return undefined - } - return result - } - - function sortProvidersByName(providers: T[]): T[] { - return [...providers].sort((a, b) => { - const nameA = a.name.toLowerCase() - const nameB = b.name.toLowerCase() - if (nameA < nameB) return -1 - if (nameA > nameB) return 1 - return 0 - }) - } - - function modelVariantsFromProviders( - providers: Array<{ id: string; models: Record }> }>, - model: { providerID: ProviderID; modelID: ModelID }, - ): string[] { - const provider = providers.find((entry) => entry.id === model.providerID) - if (!provider) return [] - const modelInfo = provider.models[model.modelID] - if (!modelInfo?.variants) return [] - return Object.keys(modelInfo.variants) - } - - function buildAvailableModels( - providers: Array<{ id: string; name: string; models: Record }>, - options: { includeVariants?: boolean } = {}, - ): ModelOption[] { - const includeVariants = options.includeVariants ?? false - return providers.flatMap((provider) => { - const unsorted: Array<{ id: string; name: string; variants?: Record }> = Object.values( - provider.models, - ) - const models = Provider.sort(unsorted) - return models.flatMap((model) => { - const base: ModelOption = { - modelId: `${provider.id}/${model.id}`, - name: `${provider.name}/${model.name}`, - } - if (!includeVariants || !model.variants) return [base] - const variants = Object.keys(model.variants).filter((variant) => variant !== DEFAULT_VARIANT_VALUE) - const variantOptions = variants.map((variant) => ({ - modelId: `${provider.id}/${model.id}/${variant}`, - name: `${provider.name}/${model.name} (${variant})`, - })) - return [base, ...variantOptions] - }) - }) - } - - function formatModelIdWithVariant( - model: { providerID: ProviderID; modelID: ModelID }, - variant: string | undefined, - availableVariants: string[], - includeVariant: boolean, - ) { - const base = `${model.providerID}/${model.modelID}` - if (!includeVariant || !variant || !availableVariants.includes(variant)) return base - return `${base}/${variant}` - } - - function buildVariantMeta(input: { - model: { providerID: ProviderID; modelID: ModelID } - variant?: string - availableVariants: string[] - }) { return { - opencode: { - modelId: `${input.model.providerID}/${input.model.modelID}`, - variant: input.variant ?? null, - availableVariants: input.availableVariants, - }, + type: "text", + text: uri, + } + } catch { + return { + type: "text", + text: uri, } } +} - function parseModelSelection( - modelId: string, - providers: Array<{ id: string; models: Record }> }>, - ): { model: { providerID: ProviderID; modelID: ModelID }; variant?: string } { - const parsed = Provider.parseModel(modelId) - const provider = providers.find((p) => p.id === parsed.providerID) - if (!provider) { - return { model: parsed, variant: undefined } - } +function getNewContent(fileOriginal: string, unifiedDiff: string): string | undefined { + const result = applyPatch(fileOriginal, unifiedDiff) + if (result === false) { + log.error("Failed to apply unified diff (context mismatch)") + return undefined + } + return result +} - // Check if modelID exists directly - if (provider.models[parsed.modelID]) { - return { model: parsed, variant: undefined } - } +function sortProvidersByName(providers: T[]): T[] { + return [...providers].sort((a, b) => { + const nameA = a.name.toLowerCase() + const nameB = b.name.toLowerCase() + if (nameA < nameB) return -1 + if (nameA > nameB) return 1 + return 0 + }) +} - // Try to extract variant from end of modelID (e.g., "claude-sonnet-4/high" -> model: "claude-sonnet-4", variant: "high") - const segments = parsed.modelID.split("/") - if (segments.length > 1) { - const candidateVariant = segments[segments.length - 1] - const baseModelId = segments.slice(0, -1).join("/") - const baseModelInfo = provider.models[baseModelId] - if (baseModelInfo?.variants && candidateVariant in baseModelInfo.variants) { - return { - model: { providerID: parsed.providerID, modelID: ModelID.make(baseModelId) }, - variant: candidateVariant, - } +function modelVariantsFromProviders( + providers: Array<{ id: string; models: Record }> }>, + model: { providerID: ProviderID; modelID: ModelID }, +): string[] { + const provider = providers.find((entry) => entry.id === model.providerID) + if (!provider) return [] + const modelInfo = provider.models[model.modelID] + if (!modelInfo?.variants) return [] + return Object.keys(modelInfo.variants) +} + +function buildAvailableModels( + providers: Array<{ id: string; name: string; models: Record }>, + options: { includeVariants?: boolean } = {}, +): ModelOption[] { + const includeVariants = options.includeVariants ?? false + return providers.flatMap((provider) => { + const unsorted: Array<{ id: string; name: string; variants?: Record }> = Object.values( + provider.models, + ) + const models = Provider.sort(unsorted) + return models.flatMap((model) => { + const base: ModelOption = { + modelId: `${provider.id}/${model.id}`, + name: `${provider.name}/${model.name}`, } - } + if (!includeVariants || !model.variants) return [base] + const variants = Object.keys(model.variants).filter((variant) => variant !== DEFAULT_VARIANT_VALUE) + const variantOptions = variants.map((variant) => ({ + modelId: `${provider.id}/${model.id}/${variant}`, + name: `${provider.name}/${model.name} (${variant})`, + })) + return [base, ...variantOptions] + }) + }) +} +function formatModelIdWithVariant( + model: { providerID: ProviderID; modelID: ModelID }, + variant: string | undefined, + availableVariants: string[], + includeVariant: boolean, +) { + const base = `${model.providerID}/${model.modelID}` + if (!includeVariant || !variant || !availableVariants.includes(variant)) return base + return `${base}/${variant}` +} + +function buildVariantMeta(input: { + model: { providerID: ProviderID; modelID: ModelID } + variant?: string + availableVariants: string[] +}) { + return { + opencode: { + modelId: `${input.model.providerID}/${input.model.modelID}`, + variant: input.variant ?? null, + availableVariants: input.availableVariants, + }, + } +} + +function parseModelSelection( + modelId: string, + providers: Array<{ id: string; models: Record }> }>, +): { model: { providerID: ProviderID; modelID: ModelID }; variant?: string } { + const parsed = Provider.parseModel(modelId) + const provider = providers.find((p) => p.id === parsed.providerID) + if (!provider) { return { model: parsed, variant: undefined } } - function buildConfigOptions(input: { - currentModelId: string - availableModels: ModelOption[] - modes?: { availableModes: ModeOption[]; currentModeId: string } | undefined - }): SessionConfigOption[] { - const options: SessionConfigOption[] = [ - { - id: "model", - name: "Model", - category: "model", - type: "select", - currentValue: input.currentModelId, - options: input.availableModels.map((m) => ({ value: m.modelId, name: m.name })), - }, - ] - if (input.modes) { - options.push({ - id: "mode", - name: "Session Mode", - category: "mode", - type: "select", - currentValue: input.modes.currentModeId, - options: input.modes.availableModes.map((m) => ({ - value: m.id, - name: m.name, - ...(m.description ? { description: m.description } : {}), - })), - }) - } - return options + // Check if modelID exists directly + if (provider.models[parsed.modelID]) { + return { model: parsed, variant: undefined } } + + // Try to extract variant from end of modelID (e.g., "claude-sonnet-4/high" -> model: "claude-sonnet-4", variant: "high") + const segments = parsed.modelID.split("/") + if (segments.length > 1) { + const candidateVariant = segments[segments.length - 1] + const baseModelId = segments.slice(0, -1).join("/") + const baseModelInfo = provider.models[baseModelId] + if (baseModelInfo?.variants && candidateVariant in baseModelInfo.variants) { + return { + model: { providerID: parsed.providerID, modelID: ModelID.make(baseModelId) }, + variant: candidateVariant, + } + } + } + + return { model: parsed, variant: undefined } } + +function buildConfigOptions(input: { + currentModelId: string + availableModels: ModelOption[] + modes?: { availableModes: ModeOption[]; currentModeId: string } | undefined +}): SessionConfigOption[] { + const options: SessionConfigOption[] = [ + { + id: "model", + name: "Model", + category: "model", + type: "select", + currentValue: input.currentModelId, + options: input.availableModels.map((m) => ({ value: m.modelId, name: m.name })), + }, + ] + if (input.modes) { + options.push({ + id: "mode", + name: "Session Mode", + category: "mode", + type: "select", + currentValue: input.modes.currentModeId, + options: input.modes.availableModes.map((m) => ({ + value: m.id, + name: m.name, + ...(m.description ? { description: m.description } : {}), + })), + }) + } + return options +} + +export * as ACP from "./agent" From cde105e7a8832b9c6d9d0a43d5699b4768156533 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:01:09 -0400 Subject: [PATCH 019/335] refactor: unwrap CopilotModels namespace + self-reexport (#22947) --- .../src/plugin/github-copilot/models.ts | 266 +++++++++--------- 1 file changed, 133 insertions(+), 133 deletions(-) diff --git a/packages/opencode/src/plugin/github-copilot/models.ts b/packages/opencode/src/plugin/github-copilot/models.ts index dfd6ceceaa..71d21afbe4 100644 --- a/packages/opencode/src/plugin/github-copilot/models.ts +++ b/packages/opencode/src/plugin/github-copilot/models.ts @@ -1,146 +1,146 @@ import { z } from "zod" import type { Model } from "@opencode-ai/sdk/v2" -export namespace CopilotModels { - export const schema = z.object({ - data: z.array( - z.object({ - model_picker_enabled: z.boolean(), - id: z.string(), - name: z.string(), - // every version looks like: `{model.id}-YYYY-MM-DD` - version: z.string(), - supported_endpoints: z.array(z.string()).optional(), - capabilities: z.object({ - family: z.string(), - limits: z.object({ - max_context_window_tokens: z.number(), - max_output_tokens: z.number(), - max_prompt_tokens: z.number(), - vision: z - .object({ - max_prompt_image_size: z.number(), - max_prompt_images: z.number(), - supported_media_types: z.array(z.string()), - }) - .optional(), - }), - supports: z.object({ - adaptive_thinking: z.boolean().optional(), - max_thinking_budget: z.number().optional(), - min_thinking_budget: z.number().optional(), - reasoning_effort: z.array(z.string()).optional(), - streaming: z.boolean(), - structured_outputs: z.boolean().optional(), - tool_calls: z.boolean(), - vision: z.boolean().optional(), - }), +export const schema = z.object({ + data: z.array( + z.object({ + model_picker_enabled: z.boolean(), + id: z.string(), + name: z.string(), + // every version looks like: `{model.id}-YYYY-MM-DD` + version: z.string(), + supported_endpoints: z.array(z.string()).optional(), + capabilities: z.object({ + family: z.string(), + limits: z.object({ + max_context_window_tokens: z.number(), + max_output_tokens: z.number(), + max_prompt_tokens: z.number(), + vision: z + .object({ + max_prompt_image_size: z.number(), + max_prompt_images: z.number(), + supported_media_types: z.array(z.string()), + }) + .optional(), + }), + supports: z.object({ + adaptive_thinking: z.boolean().optional(), + max_thinking_budget: z.number().optional(), + min_thinking_budget: z.number().optional(), + reasoning_effort: z.array(z.string()).optional(), + streaming: z.boolean(), + structured_outputs: z.boolean().optional(), + tool_calls: z.boolean(), + vision: z.boolean().optional(), }), }), - ), - }) + }), + ), +}) - type Item = z.infer["data"][number] +type Item = z.infer["data"][number] - function build(key: string, remote: Item, url: string, prev?: Model): Model { - const reasoning = - !!remote.capabilities.supports.adaptive_thinking || - !!remote.capabilities.supports.reasoning_effort?.length || - remote.capabilities.supports.max_thinking_budget !== undefined || - remote.capabilities.supports.min_thinking_budget !== undefined - const image = - (remote.capabilities.supports.vision ?? false) || - (remote.capabilities.limits.vision?.supported_media_types ?? []).some((item) => item.startsWith("image/")) +function build(key: string, remote: Item, url: string, prev?: Model): Model { + const reasoning = + !!remote.capabilities.supports.adaptive_thinking || + !!remote.capabilities.supports.reasoning_effort?.length || + remote.capabilities.supports.max_thinking_budget !== undefined || + remote.capabilities.supports.min_thinking_budget !== undefined + const image = + (remote.capabilities.supports.vision ?? false) || + (remote.capabilities.limits.vision?.supported_media_types ?? []).some((item) => item.startsWith("image/")) - const isMsgApi = remote.supported_endpoints?.includes("/v1/messages") + const isMsgApi = remote.supported_endpoints?.includes("/v1/messages") - return { - id: key, - providerID: "github-copilot", - api: { - id: remote.id, - url: isMsgApi ? `${url}/v1` : url, - npm: isMsgApi ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot", + return { + id: key, + providerID: "github-copilot", + api: { + id: remote.id, + url: isMsgApi ? `${url}/v1` : url, + npm: isMsgApi ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot", + }, + // API response wins + status: "active", + limit: { + context: remote.capabilities.limits.max_context_window_tokens, + input: remote.capabilities.limits.max_prompt_tokens, + output: remote.capabilities.limits.max_output_tokens, + }, + capabilities: { + temperature: prev?.capabilities.temperature ?? true, + reasoning: prev?.capabilities.reasoning ?? reasoning, + attachment: prev?.capabilities.attachment ?? true, + toolcall: remote.capabilities.supports.tool_calls, + input: { + text: true, + audio: false, + image, + video: false, + pdf: false, }, - // API response wins - status: "active", - limit: { - context: remote.capabilities.limits.max_context_window_tokens, - input: remote.capabilities.limits.max_prompt_tokens, - output: remote.capabilities.limits.max_output_tokens, + output: { + text: true, + audio: false, + image: false, + video: false, + pdf: false, }, - capabilities: { - temperature: prev?.capabilities.temperature ?? true, - reasoning: prev?.capabilities.reasoning ?? reasoning, - attachment: prev?.capabilities.attachment ?? true, - toolcall: remote.capabilities.supports.tool_calls, - input: { - text: true, - audio: false, - image, - video: false, - pdf: false, - }, - output: { - text: true, - audio: false, - image: false, - video: false, - pdf: false, - }, - interleaved: false, - }, - // existing wins - family: prev?.family ?? remote.capabilities.family, - name: prev?.name ?? remote.name, - cost: { - input: 0, - output: 0, - cache: { read: 0, write: 0 }, - }, - options: prev?.options ?? {}, - headers: prev?.headers ?? {}, - release_date: - prev?.release_date ?? - (remote.version.startsWith(`${remote.id}-`) ? remote.version.slice(remote.id.length + 1) : remote.version), - variants: prev?.variants ?? {}, - } - } - - export async function get( - baseURL: string, - headers: HeadersInit = {}, - existing: Record = {}, - ): Promise> { - const data = await fetch(`${baseURL}/models`, { - headers, - signal: AbortSignal.timeout(5_000), - }).then(async (res) => { - if (!res.ok) { - throw new Error(`Failed to fetch models: ${res.status}`) - } - return schema.parse(await res.json()) - }) - - const result = { ...existing } - const remote = new Map(data.data.filter((m) => m.model_picker_enabled).map((m) => [m.id, m] as const)) - - // prune existing models whose api.id isn't in the endpoint response - for (const [key, model] of Object.entries(result)) { - const m = remote.get(model.api.id) - if (!m) { - delete result[key] - continue - } - result[key] = build(key, m, baseURL, model) - } - - // add new endpoint models not already keyed in result - for (const [id, m] of remote) { - if (id in result) continue - result[id] = build(id, m, baseURL) - } - - return result + interleaved: false, + }, + // existing wins + family: prev?.family ?? remote.capabilities.family, + name: prev?.name ?? remote.name, + cost: { + input: 0, + output: 0, + cache: { read: 0, write: 0 }, + }, + options: prev?.options ?? {}, + headers: prev?.headers ?? {}, + release_date: + prev?.release_date ?? + (remote.version.startsWith(`${remote.id}-`) ? remote.version.slice(remote.id.length + 1) : remote.version), + variants: prev?.variants ?? {}, } } + +export async function get( + baseURL: string, + headers: HeadersInit = {}, + existing: Record = {}, +): Promise> { + const data = await fetch(`${baseURL}/models`, { + headers, + signal: AbortSignal.timeout(5_000), + }).then(async (res) => { + if (!res.ok) { + throw new Error(`Failed to fetch models: ${res.status}`) + } + return schema.parse(await res.json()) + }) + + const result = { ...existing } + const remote = new Map(data.data.filter((m) => m.model_picker_enabled).map((m) => [m.id, m] as const)) + + // prune existing models whose api.id isn't in the endpoint response + for (const [key, model] of Object.entries(result)) { + const m = remote.get(model.api.id) + if (!m) { + delete result[key] + continue + } + result[key] = build(key, m, baseURL, model) + } + + // add new endpoint models not already keyed in result + for (const [id, m] of remote) { + if (id in result) continue + result[id] = build(id, m, baseURL) + } + + return result +} + +export * as CopilotModels from "./models" From fdd5b77bfd9c525a2ae6656a011c1419748761e3 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:01:12 -0400 Subject: [PATCH 020/335] refactor: unwrap McpAuth namespace + self-reexport (#22942) --- packages/opencode/src/mcp/auth.ts | 266 +++++++++++++++--------------- 1 file changed, 133 insertions(+), 133 deletions(-) diff --git a/packages/opencode/src/mcp/auth.ts b/packages/opencode/src/mcp/auth.ts index 85f9e1d8c9..efb046d7a7 100644 --- a/packages/opencode/src/mcp/auth.ts +++ b/packages/opencode/src/mcp/auth.ts @@ -4,141 +4,141 @@ import { Global } from "../global" import { Effect, Layer, Context } from "effect" import { AppFileSystem } from "@opencode-ai/shared/filesystem" -export namespace McpAuth { - export const Tokens = z.object({ - accessToken: z.string(), - refreshToken: z.string().optional(), - expiresAt: z.number().optional(), - scope: z.string().optional(), - }) - export type Tokens = z.infer +export const Tokens = z.object({ + accessToken: z.string(), + refreshToken: z.string().optional(), + expiresAt: z.number().optional(), + scope: z.string().optional(), +}) +export type Tokens = z.infer - export const ClientInfo = z.object({ - clientId: z.string(), - clientSecret: z.string().optional(), - clientIdIssuedAt: z.number().optional(), - clientSecretExpiresAt: z.number().optional(), - }) - export type ClientInfo = z.infer +export const ClientInfo = z.object({ + clientId: z.string(), + clientSecret: z.string().optional(), + clientIdIssuedAt: z.number().optional(), + clientSecretExpiresAt: z.number().optional(), +}) +export type ClientInfo = z.infer - export const Entry = z.object({ - tokens: Tokens.optional(), - clientInfo: ClientInfo.optional(), - codeVerifier: z.string().optional(), - oauthState: z.string().optional(), - serverUrl: z.string().optional(), - }) - export type Entry = z.infer +export const Entry = z.object({ + tokens: Tokens.optional(), + clientInfo: ClientInfo.optional(), + codeVerifier: z.string().optional(), + oauthState: z.string().optional(), + serverUrl: z.string().optional(), +}) +export type Entry = z.infer - const filepath = path.join(Global.Path.data, "mcp-auth.json") +const filepath = path.join(Global.Path.data, "mcp-auth.json") - export interface Interface { - readonly all: () => Effect.Effect> - readonly get: (mcpName: string) => Effect.Effect - readonly getForUrl: (mcpName: string, serverUrl: string) => Effect.Effect - readonly set: (mcpName: string, entry: Entry, serverUrl?: string) => Effect.Effect - readonly remove: (mcpName: string) => Effect.Effect - readonly updateTokens: (mcpName: string, tokens: Tokens, serverUrl?: string) => Effect.Effect - readonly updateClientInfo: (mcpName: string, clientInfo: ClientInfo, serverUrl?: string) => Effect.Effect - readonly updateCodeVerifier: (mcpName: string, codeVerifier: string) => Effect.Effect - readonly clearCodeVerifier: (mcpName: string) => Effect.Effect - readonly updateOAuthState: (mcpName: string, oauthState: string) => Effect.Effect - readonly getOAuthState: (mcpName: string) => Effect.Effect - readonly clearOAuthState: (mcpName: string) => Effect.Effect - readonly isTokenExpired: (mcpName: string) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/McpAuth") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const fs = yield* AppFileSystem.Service - - const all = Effect.fn("McpAuth.all")(function* () { - return yield* fs.readJson(filepath).pipe( - Effect.map((data) => data as Record), - Effect.catch(() => Effect.succeed({} as Record)), - ) - }) - - const get = Effect.fn("McpAuth.get")(function* (mcpName: string) { - const data = yield* all() - return data[mcpName] - }) - - const getForUrl = Effect.fn("McpAuth.getForUrl")(function* (mcpName: string, serverUrl: string) { - const entry = yield* get(mcpName) - if (!entry) return undefined - if (!entry.serverUrl) return undefined - if (entry.serverUrl !== serverUrl) return undefined - return entry - }) - - const set = Effect.fn("McpAuth.set")(function* (mcpName: string, entry: Entry, serverUrl?: string) { - const data = yield* all() - if (serverUrl) entry.serverUrl = serverUrl - yield* fs.writeJson(filepath, { ...data, [mcpName]: entry }, 0o600).pipe(Effect.orDie) - }) - - const remove = Effect.fn("McpAuth.remove")(function* (mcpName: string) { - const data = yield* all() - delete data[mcpName] - yield* fs.writeJson(filepath, data, 0o600).pipe(Effect.orDie) - }) - - const updateField = (field: K, spanName: string) => - Effect.fn(`McpAuth.${spanName}`)(function* (mcpName: string, value: NonNullable, serverUrl?: string) { - const entry = (yield* get(mcpName)) ?? {} - entry[field] = value - yield* set(mcpName, entry, serverUrl) - }) - - const clearField = (field: K, spanName: string) => - Effect.fn(`McpAuth.${spanName}`)(function* (mcpName: string) { - const entry = yield* get(mcpName) - if (entry) { - delete entry[field] - yield* set(mcpName, entry) - } - }) - - const updateTokens = updateField("tokens", "updateTokens") - const updateClientInfo = updateField("clientInfo", "updateClientInfo") - const updateCodeVerifier = updateField("codeVerifier", "updateCodeVerifier") - const updateOAuthState = updateField("oauthState", "updateOAuthState") - const clearCodeVerifier = clearField("codeVerifier", "clearCodeVerifier") - const clearOAuthState = clearField("oauthState", "clearOAuthState") - - const getOAuthState = Effect.fn("McpAuth.getOAuthState")(function* (mcpName: string) { - const entry = yield* get(mcpName) - return entry?.oauthState - }) - - const isTokenExpired = Effect.fn("McpAuth.isTokenExpired")(function* (mcpName: string) { - const entry = yield* get(mcpName) - if (!entry?.tokens) return null - if (!entry.tokens.expiresAt) return false - return entry.tokens.expiresAt < Date.now() / 1000 - }) - - return Service.of({ - all, - get, - getForUrl, - set, - remove, - updateTokens, - updateClientInfo, - updateCodeVerifier, - clearCodeVerifier, - updateOAuthState, - getOAuthState, - clearOAuthState, - isTokenExpired, - }) - }), - ) - - export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) +export interface Interface { + readonly all: () => Effect.Effect> + readonly get: (mcpName: string) => Effect.Effect + readonly getForUrl: (mcpName: string, serverUrl: string) => Effect.Effect + readonly set: (mcpName: string, entry: Entry, serverUrl?: string) => Effect.Effect + readonly remove: (mcpName: string) => Effect.Effect + readonly updateTokens: (mcpName: string, tokens: Tokens, serverUrl?: string) => Effect.Effect + readonly updateClientInfo: (mcpName: string, clientInfo: ClientInfo, serverUrl?: string) => Effect.Effect + readonly updateCodeVerifier: (mcpName: string, codeVerifier: string) => Effect.Effect + readonly clearCodeVerifier: (mcpName: string) => Effect.Effect + readonly updateOAuthState: (mcpName: string, oauthState: string) => Effect.Effect + readonly getOAuthState: (mcpName: string) => Effect.Effect + readonly clearOAuthState: (mcpName: string) => Effect.Effect + readonly isTokenExpired: (mcpName: string) => Effect.Effect } + +export class Service extends Context.Service()("@opencode/McpAuth") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + + const all = Effect.fn("McpAuth.all")(function* () { + return yield* fs.readJson(filepath).pipe( + Effect.map((data) => data as Record), + Effect.catch(() => Effect.succeed({} as Record)), + ) + }) + + const get = Effect.fn("McpAuth.get")(function* (mcpName: string) { + const data = yield* all() + return data[mcpName] + }) + + const getForUrl = Effect.fn("McpAuth.getForUrl")(function* (mcpName: string, serverUrl: string) { + const entry = yield* get(mcpName) + if (!entry) return undefined + if (!entry.serverUrl) return undefined + if (entry.serverUrl !== serverUrl) return undefined + return entry + }) + + const set = Effect.fn("McpAuth.set")(function* (mcpName: string, entry: Entry, serverUrl?: string) { + const data = yield* all() + if (serverUrl) entry.serverUrl = serverUrl + yield* fs.writeJson(filepath, { ...data, [mcpName]: entry }, 0o600).pipe(Effect.orDie) + }) + + const remove = Effect.fn("McpAuth.remove")(function* (mcpName: string) { + const data = yield* all() + delete data[mcpName] + yield* fs.writeJson(filepath, data, 0o600).pipe(Effect.orDie) + }) + + const updateField = (field: K, spanName: string) => + Effect.fn(`McpAuth.${spanName}`)(function* (mcpName: string, value: NonNullable, serverUrl?: string) { + const entry = (yield* get(mcpName)) ?? {} + entry[field] = value + yield* set(mcpName, entry, serverUrl) + }) + + const clearField = (field: K, spanName: string) => + Effect.fn(`McpAuth.${spanName}`)(function* (mcpName: string) { + const entry = yield* get(mcpName) + if (entry) { + delete entry[field] + yield* set(mcpName, entry) + } + }) + + const updateTokens = updateField("tokens", "updateTokens") + const updateClientInfo = updateField("clientInfo", "updateClientInfo") + const updateCodeVerifier = updateField("codeVerifier", "updateCodeVerifier") + const updateOAuthState = updateField("oauthState", "updateOAuthState") + const clearCodeVerifier = clearField("codeVerifier", "clearCodeVerifier") + const clearOAuthState = clearField("oauthState", "clearOAuthState") + + const getOAuthState = Effect.fn("McpAuth.getOAuthState")(function* (mcpName: string) { + const entry = yield* get(mcpName) + return entry?.oauthState + }) + + const isTokenExpired = Effect.fn("McpAuth.isTokenExpired")(function* (mcpName: string) { + const entry = yield* get(mcpName) + if (!entry?.tokens) return null + if (!entry.tokens.expiresAt) return false + return entry.tokens.expiresAt < Date.now() / 1000 + }) + + return Service.of({ + all, + get, + getForUrl, + set, + remove, + updateTokens, + updateClientInfo, + updateCodeVerifier, + clearCodeVerifier, + updateOAuthState, + getOAuthState, + clearOAuthState, + isTokenExpired, + }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) + +export * as McpAuth from "./auth" From f6dbb2f3e0de46d2a5e07618548c94259889a22a Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:01:37 -0400 Subject: [PATCH 021/335] refactor: unwrap Heap namespace + self-reexport (#22931) --- packages/opencode/src/cli/heap.ts | 82 +++++++++++++++---------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/packages/opencode/src/cli/heap.ts b/packages/opencode/src/cli/heap.ts index cf1cffa800..87b7b2ebf9 100644 --- a/packages/opencode/src/cli/heap.ts +++ b/packages/opencode/src/cli/heap.ts @@ -8,52 +8,52 @@ const log = Log.create({ service: "heap" }) const MINUTE = 60_000 const LIMIT = 2 * 1024 * 1024 * 1024 -export namespace Heap { - let timer: Timer | undefined - let lock = false - let armed = true +let timer: Timer | undefined +let lock = false +let armed = true - export function start() { - if (!Flag.OPENCODE_AUTO_HEAP_SNAPSHOT) return - if (timer) return +export function start() { + if (!Flag.OPENCODE_AUTO_HEAP_SNAPSHOT) return + if (timer) return - const run = async () => { - if (lock) return + const run = async () => { + if (lock) return - const stat = process.memoryUsage() - if (stat.rss <= LIMIT) { - armed = true - return - } - if (!armed) return + const stat = process.memoryUsage() + if (stat.rss <= LIMIT) { + armed = true + return + } + if (!armed) return - lock = true - armed = false - const file = path.join( - Global.Path.log, - `heap-${process.pid}-${new Date().toISOString().replace(/[:.]/g, "")}.heapsnapshot`, - ) - log.warn("heap usage exceeded limit", { - rss: stat.rss, - heap: stat.heapUsed, - file, + lock = true + armed = false + const file = path.join( + Global.Path.log, + `heap-${process.pid}-${new Date().toISOString().replace(/[:.]/g, "")}.heapsnapshot`, + ) + log.warn("heap usage exceeded limit", { + rss: stat.rss, + heap: stat.heapUsed, + file, + }) + + await Promise.resolve() + .then(() => writeHeapSnapshot(file)) + .catch((err) => { + log.error("failed to write heap snapshot", { + error: err instanceof Error ? err.message : String(err), + file, + }) }) - await Promise.resolve() - .then(() => writeHeapSnapshot(file)) - .catch((err) => { - log.error("failed to write heap snapshot", { - error: err instanceof Error ? err.message : String(err), - file, - }) - }) - - lock = false - } - - timer = setInterval(() => { - void run() - }, MINUTE) - timer.unref?.() + lock = false } + + timer = setInterval(() => { + void run() + }, MINUTE) + timer.unref?.() } + +export * as Heap from "./heap" From 79732ab17560c59745eef6d151b4b6cc69e23163 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:01:41 -0400 Subject: [PATCH 022/335] refactor: unwrap UI namespace + self-reexport (#22951) --- packages/opencode/src/cli/ui.ts | 226 ++++++++++++++++---------------- 1 file changed, 113 insertions(+), 113 deletions(-) diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts index d735a55417..46335d24a8 100644 --- a/packages/opencode/src/cli/ui.ts +++ b/packages/opencode/src/cli/ui.ts @@ -3,131 +3,131 @@ import { EOL } from "os" import { NamedError } from "@opencode-ai/shared/util/error" import { logo as glyphs } from "./logo" -export namespace UI { - const wordmark = [ - `⠀ ▄ `, - `█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█`, - `█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀`, - `▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`, - ] +const wordmark = [ + `⠀ ▄ `, + `█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█`, + `█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀`, + `▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`, +] - export const CancelledError = NamedError.create("UICancelledError", z.void()) +export const CancelledError = NamedError.create("UICancelledError", z.void()) - export const Style = { - TEXT_HIGHLIGHT: "\x1b[96m", - TEXT_HIGHLIGHT_BOLD: "\x1b[96m\x1b[1m", - TEXT_DIM: "\x1b[90m", - TEXT_DIM_BOLD: "\x1b[90m\x1b[1m", - TEXT_NORMAL: "\x1b[0m", - TEXT_NORMAL_BOLD: "\x1b[1m", - TEXT_WARNING: "\x1b[93m", - TEXT_WARNING_BOLD: "\x1b[93m\x1b[1m", - TEXT_DANGER: "\x1b[91m", - TEXT_DANGER_BOLD: "\x1b[91m\x1b[1m", - TEXT_SUCCESS: "\x1b[92m", - TEXT_SUCCESS_BOLD: "\x1b[92m\x1b[1m", - TEXT_INFO: "\x1b[94m", - TEXT_INFO_BOLD: "\x1b[94m\x1b[1m", - } +export const Style = { + TEXT_HIGHLIGHT: "\x1b[96m", + TEXT_HIGHLIGHT_BOLD: "\x1b[96m\x1b[1m", + TEXT_DIM: "\x1b[90m", + TEXT_DIM_BOLD: "\x1b[90m\x1b[1m", + TEXT_NORMAL: "\x1b[0m", + TEXT_NORMAL_BOLD: "\x1b[1m", + TEXT_WARNING: "\x1b[93m", + TEXT_WARNING_BOLD: "\x1b[93m\x1b[1m", + TEXT_DANGER: "\x1b[91m", + TEXT_DANGER_BOLD: "\x1b[91m\x1b[1m", + TEXT_SUCCESS: "\x1b[92m", + TEXT_SUCCESS_BOLD: "\x1b[92m\x1b[1m", + TEXT_INFO: "\x1b[94m", + TEXT_INFO_BOLD: "\x1b[94m\x1b[1m", +} - export function println(...message: string[]) { - print(...message) - process.stderr.write(EOL) - } +export function println(...message: string[]) { + print(...message) + process.stderr.write(EOL) +} - export function print(...message: string[]) { - blank = false - process.stderr.write(message.join(" ")) - } +export function print(...message: string[]) { + blank = false + process.stderr.write(message.join(" ")) +} - let blank = false - export function empty() { - if (blank) return - println("" + Style.TEXT_NORMAL) - blank = true - } +let blank = false +export function empty() { + if (blank) return + println("" + Style.TEXT_NORMAL) + blank = true +} - export function logo(pad?: string) { - if (!process.stdout.isTTY && !process.stderr.isTTY) { - const result = [] - for (const row of wordmark) { - if (pad) result.push(pad) - result.push(row) - result.push(EOL) - } - return result.join("").trimEnd() - } - - const result: string[] = [] - const reset = "\x1b[0m" - const left = { - fg: "\x1b[90m", - shadow: "\x1b[38;5;235m", - bg: "\x1b[48;5;235m", - } - const right = { - fg: reset, - shadow: "\x1b[38;5;238m", - bg: "\x1b[48;5;238m", - } - const gap = " " - const draw = (line: string, fg: string, shadow: string, bg: string) => { - const parts: string[] = [] - for (const char of line) { - if (char === "_") { - parts.push(bg, " ", reset) - continue - } - if (char === "^") { - parts.push(fg, bg, "▀", reset) - continue - } - if (char === "~") { - parts.push(shadow, "▀", reset) - continue - } - if (char === " ") { - parts.push(" ") - continue - } - parts.push(fg, char, reset) - } - return parts.join("") - } - glyphs.left.forEach((row, index) => { +export function logo(pad?: string) { + if (!process.stdout.isTTY && !process.stderr.isTTY) { + const result = [] + for (const row of wordmark) { if (pad) result.push(pad) - result.push(draw(row, left.fg, left.shadow, left.bg)) - result.push(gap) - const other = glyphs.right[index] ?? "" - result.push(draw(other, right.fg, right.shadow, right.bg)) + result.push(row) result.push(EOL) - }) + } return result.join("").trimEnd() } - export async function input(prompt: string): Promise { - const readline = require("readline") - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }) - - return new Promise((resolve) => { - rl.question(prompt, (answer: string) => { - rl.close() - resolve(answer.trim()) - }) - }) + const result: string[] = [] + const reset = "\x1b[0m" + const left = { + fg: "\x1b[90m", + shadow: "\x1b[38;5;235m", + bg: "\x1b[48;5;235m", } - - export function error(message: string) { - if (message.startsWith("Error: ")) { - message = message.slice("Error: ".length) + const right = { + fg: reset, + shadow: "\x1b[38;5;238m", + bg: "\x1b[48;5;238m", + } + const gap = " " + const draw = (line: string, fg: string, shadow: string, bg: string) => { + const parts: string[] = [] + for (const char of line) { + if (char === "_") { + parts.push(bg, " ", reset) + continue + } + if (char === "^") { + parts.push(fg, bg, "▀", reset) + continue + } + if (char === "~") { + parts.push(shadow, "▀", reset) + continue + } + if (char === " ") { + parts.push(" ") + continue + } + parts.push(fg, char, reset) } - println(Style.TEXT_DANGER_BOLD + "Error: " + Style.TEXT_NORMAL + message) - } - - export function markdown(text: string): string { - return text + return parts.join("") } + glyphs.left.forEach((row, index) => { + if (pad) result.push(pad) + result.push(draw(row, left.fg, left.shadow, left.bg)) + result.push(gap) + const other = glyphs.right[index] ?? "" + result.push(draw(other, right.fg, right.shadow, right.bg)) + result.push(EOL) + }) + return result.join("").trimEnd() } + +export async function input(prompt: string): Promise { + const readline = require("readline") + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }) + + return new Promise((resolve) => { + rl.question(prompt, (answer: string) => { + rl.close() + resolve(answer.trim()) + }) + }) +} + +export function error(message: string) { + if (message.startsWith("Error: ")) { + message = message.slice("Error: ".length) + } + println(Style.TEXT_DANGER_BOLD + "Error: " + Style.TEXT_NORMAL + message) +} + +export function markdown(text: string): string { + return text +} + +export * as UI from "./ui" From fb0274446043401d48fae0aefb8e21f75a080ee8 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:01:44 -0400 Subject: [PATCH 023/335] refactor: unwrap Agent namespace + self-reexport (#22935) --- packages/opencode/src/agent/agent.ts | 766 +++++++++++++-------------- 1 file changed, 383 insertions(+), 383 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 54ca484555..07f742fe12 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -24,389 +24,389 @@ import { InstanceState } from "@/effect" import * as Option from "effect/Option" import * as OtelTracer from "@effect/opentelemetry/Tracer" -export namespace Agent { - export const Info = z - .object({ - name: z.string(), - description: z.string().optional(), - mode: z.enum(["subagent", "primary", "all"]), - native: z.boolean().optional(), - hidden: z.boolean().optional(), - topP: z.number().optional(), - temperature: z.number().optional(), - color: z.string().optional(), - permission: Permission.Ruleset.zod, - model: z - .object({ - modelID: ModelID.zod, - providerID: ProviderID.zod, - }) - .optional(), - variant: z.string().optional(), - prompt: z.string().optional(), - options: z.record(z.string(), z.any()), - steps: z.number().int().positive().optional(), - }) - .meta({ - ref: "Agent", - }) - export type Info = z.infer - - export interface Interface { - readonly get: (agent: string) => Effect.Effect - readonly list: () => Effect.Effect - readonly defaultAgent: () => Effect.Effect - readonly generate: (input: { - description: string - model?: { providerID: ProviderID; modelID: ModelID } - }) => Effect.Effect<{ - identifier: string - whenToUse: string - systemPrompt: string - }> - } - - type State = Omit - - export class Service extends Context.Service()("@opencode/Agent") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const config = yield* Config.Service - const auth = yield* Auth.Service - const plugin = yield* Plugin.Service - const skill = yield* Skill.Service - const provider = yield* Provider.Service - - const state = yield* InstanceState.make( - Effect.fn("Agent.state")(function* (_ctx) { - const cfg = yield* config.get() - const skillDirs = yield* skill.dirs() - const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))] - - const defaults = Permission.fromConfig({ - "*": "allow", - doom_loop: "ask", - external_directory: { - "*": "ask", - ...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])), - }, - question: "deny", - plan_enter: "deny", - plan_exit: "deny", - // mirrors github.com/github/gitignore Node.gitignore pattern for .env files - read: { - "*": "allow", - "*.env": "ask", - "*.env.*": "ask", - "*.env.example": "allow", - }, - }) - - const user = Permission.fromConfig(cfg.permission ?? {}) - - const agents: Record = { - build: { - name: "build", - description: "The default agent. Executes tools based on configured permissions.", - options: {}, - permission: Permission.merge( - defaults, - Permission.fromConfig({ - question: "allow", - plan_enter: "allow", - }), - user, - ), - mode: "primary", - native: true, - }, - plan: { - name: "plan", - description: "Plan mode. Disallows all edit tools.", - options: {}, - permission: Permission.merge( - defaults, - Permission.fromConfig({ - question: "allow", - plan_exit: "allow", - external_directory: { - [path.join(Global.Path.data, "plans", "*")]: "allow", - }, - edit: { - "*": "deny", - [path.join(".opencode", "plans", "*.md")]: "allow", - [path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: - "allow", - }, - }), - user, - ), - mode: "primary", - native: true, - }, - general: { - name: "general", - description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`, - permission: Permission.merge( - defaults, - Permission.fromConfig({ - todowrite: "deny", - }), - user, - ), - options: {}, - mode: "subagent", - native: true, - }, - explore: { - name: "explore", - permission: Permission.merge( - defaults, - Permission.fromConfig({ - "*": "deny", - grep: "allow", - glob: "allow", - list: "allow", - bash: "allow", - webfetch: "allow", - websearch: "allow", - codesearch: "allow", - read: "allow", - external_directory: { - "*": "ask", - ...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])), - }, - }), - user, - ), - description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`, - prompt: PROMPT_EXPLORE, - options: {}, - mode: "subagent", - native: true, - }, - compaction: { - name: "compaction", - mode: "primary", - native: true, - hidden: true, - prompt: PROMPT_COMPACTION, - permission: Permission.merge( - defaults, - Permission.fromConfig({ - "*": "deny", - }), - user, - ), - options: {}, - }, - title: { - name: "title", - mode: "primary", - options: {}, - native: true, - hidden: true, - temperature: 0.5, - permission: Permission.merge( - defaults, - Permission.fromConfig({ - "*": "deny", - }), - user, - ), - prompt: PROMPT_TITLE, - }, - summary: { - name: "summary", - mode: "primary", - options: {}, - native: true, - hidden: true, - permission: Permission.merge( - defaults, - Permission.fromConfig({ - "*": "deny", - }), - user, - ), - prompt: PROMPT_SUMMARY, - }, - } - - for (const [key, value] of Object.entries(cfg.agent ?? {})) { - if (value.disable) { - delete agents[key] - continue - } - let item = agents[key] - if (!item) - item = agents[key] = { - name: key, - mode: "all", - permission: Permission.merge(defaults, user), - options: {}, - native: false, - } - if (value.model) item.model = Provider.parseModel(value.model) - item.variant = value.variant ?? item.variant - item.prompt = value.prompt ?? item.prompt - item.description = value.description ?? item.description - item.temperature = value.temperature ?? item.temperature - item.topP = value.top_p ?? item.topP - item.mode = value.mode ?? item.mode - item.color = value.color ?? item.color - item.hidden = value.hidden ?? item.hidden - item.name = value.name ?? item.name - item.steps = value.steps ?? item.steps - item.options = mergeDeep(item.options, value.options ?? {}) - item.permission = Permission.merge(item.permission, Permission.fromConfig(value.permission ?? {})) - } - - // Ensure Truncate.GLOB is allowed unless explicitly configured - for (const name in agents) { - const agent = agents[name] - const explicit = agent.permission.some((r) => { - if (r.permission !== "external_directory") return false - if (r.action !== "deny") return false - return r.pattern === Truncate.GLOB - }) - if (explicit) continue - - agents[name].permission = Permission.merge( - agents[name].permission, - Permission.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }), - ) - } - - const get = Effect.fnUntraced(function* (agent: string) { - return agents[agent] - }) - - const list = Effect.fnUntraced(function* () { - const cfg = yield* config.get() - return pipe( - agents, - values(), - sortBy( - [(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"], - [(x) => x.name, "asc"], - ), - ) - }) - - const defaultAgent = Effect.fnUntraced(function* () { - const c = yield* config.get() - if (c.default_agent) { - const agent = agents[c.default_agent] - if (!agent) throw new Error(`default agent "${c.default_agent}" not found`) - if (agent.mode === "subagent") throw new Error(`default agent "${c.default_agent}" is a subagent`) - if (agent.hidden === true) throw new Error(`default agent "${c.default_agent}" is hidden`) - return agent.name - } - const visible = Object.values(agents).find((a) => a.mode !== "subagent" && a.hidden !== true) - if (!visible) throw new Error("no primary visible agent found") - return visible.name - }) - - return { - get, - list, - defaultAgent, - } satisfies State - }), - ) - - return Service.of({ - get: Effect.fn("Agent.get")(function* (agent: string) { - return yield* InstanceState.useEffect(state, (s) => s.get(agent)) - }), - list: Effect.fn("Agent.list")(function* () { - return yield* InstanceState.useEffect(state, (s) => s.list()) - }), - defaultAgent: Effect.fn("Agent.defaultAgent")(function* () { - return yield* InstanceState.useEffect(state, (s) => s.defaultAgent()) - }), - generate: Effect.fn("Agent.generate")(function* (input: { - description: string - model?: { providerID: ProviderID; modelID: ModelID } - }) { - const cfg = yield* config.get() - const model = input.model ?? (yield* provider.defaultModel()) - const resolved = yield* provider.getModel(model.providerID, model.modelID) - const language = yield* provider.getLanguage(resolved) - const tracer = cfg.experimental?.openTelemetry - ? Option.getOrUndefined(yield* Effect.serviceOption(OtelTracer.OtelTracer)) - : undefined - - const system = [PROMPT_GENERATE] - yield* plugin.trigger("experimental.chat.system.transform", { model: resolved }, { system }) - const existing = yield* InstanceState.useEffect(state, (s) => s.list()) - - // TODO: clean this up so provider specific logic doesnt bleed over - const authInfo = yield* auth.get(model.providerID).pipe(Effect.orDie) - const isOpenaiOauth = model.providerID === "openai" && authInfo?.type === "oauth" - - const params = { - experimental_telemetry: { - isEnabled: cfg.experimental?.openTelemetry, - tracer, - metadata: { - userId: cfg.username ?? "unknown", - }, - }, - temperature: 0.3, - messages: [ - ...(isOpenaiOauth - ? [] - : system.map( - (item): ModelMessage => ({ - role: "system", - content: item, - }), - )), - { - role: "user", - content: `Create an agent configuration based on this request: "${input.description}".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`, - }, - ], - model: language, - schema: z.object({ - identifier: z.string(), - whenToUse: z.string(), - systemPrompt: z.string(), - }), - } satisfies Parameters[0] - - if (isOpenaiOauth) { - return yield* Effect.promise(async () => { - const result = streamObject({ - ...params, - providerOptions: ProviderTransform.providerOptions(resolved, { - instructions: system.join("\n"), - store: false, - }), - onError: () => {}, - }) - for await (const part of result.fullStream) { - if (part.type === "error") throw part.error - } - return result.object - }) - } - - return yield* Effect.promise(() => generateObject(params).then((r) => r.object)) - }), +export const Info = z + .object({ + name: z.string(), + description: z.string().optional(), + mode: z.enum(["subagent", "primary", "all"]), + native: z.boolean().optional(), + hidden: z.boolean().optional(), + topP: z.number().optional(), + temperature: z.number().optional(), + color: z.string().optional(), + permission: Permission.Ruleset.zod, + model: z + .object({ + modelID: ModelID.zod, + providerID: ProviderID.zod, }) - }), - ) + .optional(), + variant: z.string().optional(), + prompt: z.string().optional(), + options: z.record(z.string(), z.any()), + steps: z.number().int().positive().optional(), + }) + .meta({ + ref: "Agent", + }) +export type Info = z.infer - export const defaultLayer = layer.pipe( - Layer.provide(Plugin.defaultLayer), - Layer.provide(Provider.defaultLayer), - Layer.provide(Auth.defaultLayer), - Layer.provide(Config.defaultLayer), - Layer.provide(Skill.defaultLayer), - ) +export interface Interface { + readonly get: (agent: string) => Effect.Effect + readonly list: () => Effect.Effect + readonly defaultAgent: () => Effect.Effect + readonly generate: (input: { + description: string + model?: { providerID: ProviderID; modelID: ModelID } + }) => Effect.Effect<{ + identifier: string + whenToUse: string + systemPrompt: string + }> } + +type State = Omit + +export class Service extends Context.Service()("@opencode/Agent") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const config = yield* Config.Service + const auth = yield* Auth.Service + const plugin = yield* Plugin.Service + const skill = yield* Skill.Service + const provider = yield* Provider.Service + + const state = yield* InstanceState.make( + Effect.fn("Agent.state")(function* (_ctx) { + const cfg = yield* config.get() + const skillDirs = yield* skill.dirs() + const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))] + + const defaults = Permission.fromConfig({ + "*": "allow", + doom_loop: "ask", + external_directory: { + "*": "ask", + ...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])), + }, + question: "deny", + plan_enter: "deny", + plan_exit: "deny", + // mirrors github.com/github/gitignore Node.gitignore pattern for .env files + read: { + "*": "allow", + "*.env": "ask", + "*.env.*": "ask", + "*.env.example": "allow", + }, + }) + + const user = Permission.fromConfig(cfg.permission ?? {}) + + const agents: Record = { + build: { + name: "build", + description: "The default agent. Executes tools based on configured permissions.", + options: {}, + permission: Permission.merge( + defaults, + Permission.fromConfig({ + question: "allow", + plan_enter: "allow", + }), + user, + ), + mode: "primary", + native: true, + }, + plan: { + name: "plan", + description: "Plan mode. Disallows all edit tools.", + options: {}, + permission: Permission.merge( + defaults, + Permission.fromConfig({ + question: "allow", + plan_exit: "allow", + external_directory: { + [path.join(Global.Path.data, "plans", "*")]: "allow", + }, + edit: { + "*": "deny", + [path.join(".opencode", "plans", "*.md")]: "allow", + [path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: + "allow", + }, + }), + user, + ), + mode: "primary", + native: true, + }, + general: { + name: "general", + description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`, + permission: Permission.merge( + defaults, + Permission.fromConfig({ + todowrite: "deny", + }), + user, + ), + options: {}, + mode: "subagent", + native: true, + }, + explore: { + name: "explore", + permission: Permission.merge( + defaults, + Permission.fromConfig({ + "*": "deny", + grep: "allow", + glob: "allow", + list: "allow", + bash: "allow", + webfetch: "allow", + websearch: "allow", + codesearch: "allow", + read: "allow", + external_directory: { + "*": "ask", + ...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])), + }, + }), + user, + ), + description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`, + prompt: PROMPT_EXPLORE, + options: {}, + mode: "subagent", + native: true, + }, + compaction: { + name: "compaction", + mode: "primary", + native: true, + hidden: true, + prompt: PROMPT_COMPACTION, + permission: Permission.merge( + defaults, + Permission.fromConfig({ + "*": "deny", + }), + user, + ), + options: {}, + }, + title: { + name: "title", + mode: "primary", + options: {}, + native: true, + hidden: true, + temperature: 0.5, + permission: Permission.merge( + defaults, + Permission.fromConfig({ + "*": "deny", + }), + user, + ), + prompt: PROMPT_TITLE, + }, + summary: { + name: "summary", + mode: "primary", + options: {}, + native: true, + hidden: true, + permission: Permission.merge( + defaults, + Permission.fromConfig({ + "*": "deny", + }), + user, + ), + prompt: PROMPT_SUMMARY, + }, + } + + for (const [key, value] of Object.entries(cfg.agent ?? {})) { + if (value.disable) { + delete agents[key] + continue + } + let item = agents[key] + if (!item) + item = agents[key] = { + name: key, + mode: "all", + permission: Permission.merge(defaults, user), + options: {}, + native: false, + } + if (value.model) item.model = Provider.parseModel(value.model) + item.variant = value.variant ?? item.variant + item.prompt = value.prompt ?? item.prompt + item.description = value.description ?? item.description + item.temperature = value.temperature ?? item.temperature + item.topP = value.top_p ?? item.topP + item.mode = value.mode ?? item.mode + item.color = value.color ?? item.color + item.hidden = value.hidden ?? item.hidden + item.name = value.name ?? item.name + item.steps = value.steps ?? item.steps + item.options = mergeDeep(item.options, value.options ?? {}) + item.permission = Permission.merge(item.permission, Permission.fromConfig(value.permission ?? {})) + } + + // Ensure Truncate.GLOB is allowed unless explicitly configured + for (const name in agents) { + const agent = agents[name] + const explicit = agent.permission.some((r) => { + if (r.permission !== "external_directory") return false + if (r.action !== "deny") return false + return r.pattern === Truncate.GLOB + }) + if (explicit) continue + + agents[name].permission = Permission.merge( + agents[name].permission, + Permission.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }), + ) + } + + const get = Effect.fnUntraced(function* (agent: string) { + return agents[agent] + }) + + const list = Effect.fnUntraced(function* () { + const cfg = yield* config.get() + return pipe( + agents, + values(), + sortBy( + [(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"], + [(x) => x.name, "asc"], + ), + ) + }) + + const defaultAgent = Effect.fnUntraced(function* () { + const c = yield* config.get() + if (c.default_agent) { + const agent = agents[c.default_agent] + if (!agent) throw new Error(`default agent "${c.default_agent}" not found`) + if (agent.mode === "subagent") throw new Error(`default agent "${c.default_agent}" is a subagent`) + if (agent.hidden === true) throw new Error(`default agent "${c.default_agent}" is hidden`) + return agent.name + } + const visible = Object.values(agents).find((a) => a.mode !== "subagent" && a.hidden !== true) + if (!visible) throw new Error("no primary visible agent found") + return visible.name + }) + + return { + get, + list, + defaultAgent, + } satisfies State + }), + ) + + return Service.of({ + get: Effect.fn("Agent.get")(function* (agent: string) { + return yield* InstanceState.useEffect(state, (s) => s.get(agent)) + }), + list: Effect.fn("Agent.list")(function* () { + return yield* InstanceState.useEffect(state, (s) => s.list()) + }), + defaultAgent: Effect.fn("Agent.defaultAgent")(function* () { + return yield* InstanceState.useEffect(state, (s) => s.defaultAgent()) + }), + generate: Effect.fn("Agent.generate")(function* (input: { + description: string + model?: { providerID: ProviderID; modelID: ModelID } + }) { + const cfg = yield* config.get() + const model = input.model ?? (yield* provider.defaultModel()) + const resolved = yield* provider.getModel(model.providerID, model.modelID) + const language = yield* provider.getLanguage(resolved) + const tracer = cfg.experimental?.openTelemetry + ? Option.getOrUndefined(yield* Effect.serviceOption(OtelTracer.OtelTracer)) + : undefined + + const system = [PROMPT_GENERATE] + yield* plugin.trigger("experimental.chat.system.transform", { model: resolved }, { system }) + const existing = yield* InstanceState.useEffect(state, (s) => s.list()) + + // TODO: clean this up so provider specific logic doesnt bleed over + const authInfo = yield* auth.get(model.providerID).pipe(Effect.orDie) + const isOpenaiOauth = model.providerID === "openai" && authInfo?.type === "oauth" + + const params = { + experimental_telemetry: { + isEnabled: cfg.experimental?.openTelemetry, + tracer, + metadata: { + userId: cfg.username ?? "unknown", + }, + }, + temperature: 0.3, + messages: [ + ...(isOpenaiOauth + ? [] + : system.map( + (item): ModelMessage => ({ + role: "system", + content: item, + }), + )), + { + role: "user", + content: `Create an agent configuration based on this request: "${input.description}".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`, + }, + ], + model: language, + schema: z.object({ + identifier: z.string(), + whenToUse: z.string(), + systemPrompt: z.string(), + }), + } satisfies Parameters[0] + + if (isOpenaiOauth) { + return yield* Effect.promise(async () => { + const result = streamObject({ + ...params, + providerOptions: ProviderTransform.providerOptions(resolved, { + instructions: system.join("\n"), + store: false, + }), + onError: () => {}, + }) + for await (const part of result.fullStream) { + if (part.type === "error") throw part.error + } + return result.object + }) + } + + return yield* Effect.promise(() => generateObject(params).then((r) => r.object)) + }), + }) + }), +) + +export const defaultLayer = layer.pipe( + Layer.provide(Plugin.defaultLayer), + Layer.provide(Provider.defaultLayer), + Layer.provide(Auth.defaultLayer), + Layer.provide(Config.defaultLayer), + Layer.provide(Skill.defaultLayer), +) + +export * as Agent from "./agent" From 974fa1b8b1990c2e51172e32ef321e5fdce0843d Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:02:05 -0400 Subject: [PATCH 024/335] refactor: unwrap PluginMeta namespace + self-reexport (#22945) --- packages/opencode/src/plugin/meta.ts | 312 +++++++++++++-------------- 1 file changed, 156 insertions(+), 156 deletions(-) diff --git a/packages/opencode/src/plugin/meta.ts b/packages/opencode/src/plugin/meta.ts index 89955d1dfb..86ad8fbab1 100644 --- a/packages/opencode/src/plugin/meta.ts +++ b/packages/opencode/src/plugin/meta.ts @@ -8,181 +8,181 @@ import { Flock } from "@opencode-ai/shared/util/flock" import { parsePluginSpecifier, pluginSource } from "./shared" -export namespace PluginMeta { - type Source = "file" | "npm" +type Source = "file" | "npm" - export type Theme = { - src: string - dest: string - mtime?: number - size?: number - } +export type Theme = { + src: string + dest: string + mtime?: number + size?: number +} - export type Entry = { - id: string - source: Source - spec: string - target: string - requested?: string - version?: string - modified?: number - first_time: number - last_time: number - time_changed: number - load_count: number - fingerprint: string - themes?: Record - } +export type Entry = { + id: string + source: Source + spec: string + target: string + requested?: string + version?: string + modified?: number + first_time: number + last_time: number + time_changed: number + load_count: number + fingerprint: string + themes?: Record +} - export type State = "first" | "updated" | "same" +export type State = "first" | "updated" | "same" - export type Touch = { - spec: string - target: string - id: string - } +export type Touch = { + spec: string + target: string + id: string +} - type Store = Record - type Core = Omit - type Row = Touch & { core: Core } +type Store = Record +type Core = Omit +type Row = Touch & { core: Core } - function storePath() { - return Flag.OPENCODE_PLUGIN_META_FILE ?? path.join(Global.Path.state, "plugin-meta.json") - } +function storePath() { + return Flag.OPENCODE_PLUGIN_META_FILE ?? path.join(Global.Path.state, "plugin-meta.json") +} - function lock(file: string) { - return `plugin-meta:${file}` - } +function lock(file: string) { + return `plugin-meta:${file}` +} - function fileTarget(spec: string, target: string) { - if (spec.startsWith("file://")) return fileURLToPath(spec) - if (target.startsWith("file://")) return fileURLToPath(target) - return - } +function fileTarget(spec: string, target: string) { + if (spec.startsWith("file://")) return fileURLToPath(spec) + if (target.startsWith("file://")) return fileURLToPath(target) + return +} - async function modifiedAt(file: string) { - const stat = await Filesystem.statAsync(file) - if (!stat) return - const mtime = stat.mtimeMs - return Math.floor(typeof mtime === "bigint" ? Number(mtime) : mtime) - } +async function modifiedAt(file: string) { + const stat = await Filesystem.statAsync(file) + if (!stat) return + const mtime = stat.mtimeMs + return Math.floor(typeof mtime === "bigint" ? Number(mtime) : mtime) +} - function resolvedTarget(target: string) { - if (target.startsWith("file://")) return fileURLToPath(target) - return target - } +function resolvedTarget(target: string) { + if (target.startsWith("file://")) return fileURLToPath(target) + return target +} - async function npmVersion(target: string) { - const resolved = resolvedTarget(target) - const stat = await Filesystem.statAsync(resolved) - const dir = stat?.isDirectory() ? resolved : path.dirname(resolved) - return Filesystem.readJson<{ version?: string }>(path.join(dir, "package.json")) - .then((item) => item.version) - .catch(() => undefined) - } - - async function entryCore(item: Touch): Promise { - const spec = item.spec - const target = item.target - const source = pluginSource(spec) - if (source === "file") { - const file = fileTarget(spec, target) - return { - id: item.id, - source, - spec, - target, - modified: file ? await modifiedAt(file) : undefined, - } - } +async function npmVersion(target: string) { + const resolved = resolvedTarget(target) + const stat = await Filesystem.statAsync(resolved) + const dir = stat?.isDirectory() ? resolved : path.dirname(resolved) + return Filesystem.readJson<{ version?: string }>(path.join(dir, "package.json")) + .then((item) => item.version) + .catch(() => undefined) +} +async function entryCore(item: Touch): Promise { + const spec = item.spec + const target = item.target + const source = pluginSource(spec) + if (source === "file") { + const file = fileTarget(spec, target) return { id: item.id, source, spec, target, - requested: parsePluginSpecifier(spec).version, - version: await npmVersion(target), + modified: file ? await modifiedAt(file) : undefined, } } - function fingerprint(value: Core) { - if (value.source === "file") return [value.target, value.modified ?? ""].join("|") - return [value.target, value.requested ?? "", value.version ?? ""].join("|") - } - - async function read(file: string): Promise { - return Filesystem.readJson(file).catch(() => ({}) as Store) - } - - async function row(item: Touch): Promise { - return { - ...item, - core: await entryCore(item), - } - } - - function next(prev: Entry | undefined, core: Core, now: number): { state: State; entry: Entry } { - const entry: Entry = { - ...core, - first_time: prev?.first_time ?? now, - last_time: now, - time_changed: prev?.time_changed ?? now, - load_count: (prev?.load_count ?? 0) + 1, - fingerprint: fingerprint(core), - themes: prev?.themes, - } - const state: State = !prev ? "first" : prev.fingerprint === entry.fingerprint ? "same" : "updated" - if (state === "updated") entry.time_changed = now - return { - state, - entry, - } - } - - export async function touchMany(items: Touch[]): Promise> { - if (!items.length) return [] - const file = storePath() - const rows = await Promise.all(items.map((item) => row(item))) - - return Flock.withLock(lock(file), async () => { - const store = await read(file) - const now = Date.now() - const out: Array<{ state: State; entry: Entry }> = [] - for (const item of rows) { - const hit = next(store[item.id], item.core, now) - store[item.id] = hit.entry - out.push(hit) - } - await Filesystem.writeJson(file, store) - return out - }) - } - - export async function touch(spec: string, target: string, id: string): Promise<{ state: State; entry: Entry }> { - return touchMany([{ spec, target, id }]).then((item) => { - const hit = item[0] - if (hit) return hit - throw new Error("Failed to touch plugin metadata.") - }) - } - - export async function setTheme(id: string, name: string, theme: Theme): Promise { - const file = storePath() - await Flock.withLock(lock(file), async () => { - const store = await read(file) - const entry = store[id] - if (!entry) return - entry.themes = { - ...entry.themes, - [name]: theme, - } - await Filesystem.writeJson(file, store) - }) - } - - export async function list(): Promise { - const file = storePath() - return Flock.withLock(lock(file), async () => read(file)) + return { + id: item.id, + source, + spec, + target, + requested: parsePluginSpecifier(spec).version, + version: await npmVersion(target), } } + +function fingerprint(value: Core) { + if (value.source === "file") return [value.target, value.modified ?? ""].join("|") + return [value.target, value.requested ?? "", value.version ?? ""].join("|") +} + +async function read(file: string): Promise { + return Filesystem.readJson(file).catch(() => ({}) as Store) +} + +async function row(item: Touch): Promise { + return { + ...item, + core: await entryCore(item), + } +} + +function next(prev: Entry | undefined, core: Core, now: number): { state: State; entry: Entry } { + const entry: Entry = { + ...core, + first_time: prev?.first_time ?? now, + last_time: now, + time_changed: prev?.time_changed ?? now, + load_count: (prev?.load_count ?? 0) + 1, + fingerprint: fingerprint(core), + themes: prev?.themes, + } + const state: State = !prev ? "first" : prev.fingerprint === entry.fingerprint ? "same" : "updated" + if (state === "updated") entry.time_changed = now + return { + state, + entry, + } +} + +export async function touchMany(items: Touch[]): Promise> { + if (!items.length) return [] + const file = storePath() + const rows = await Promise.all(items.map((item) => row(item))) + + return Flock.withLock(lock(file), async () => { + const store = await read(file) + const now = Date.now() + const out: Array<{ state: State; entry: Entry }> = [] + for (const item of rows) { + const hit = next(store[item.id], item.core, now) + store[item.id] = hit.entry + out.push(hit) + } + await Filesystem.writeJson(file, store) + return out + }) +} + +export async function touch(spec: string, target: string, id: string): Promise<{ state: State; entry: Entry }> { + return touchMany([{ spec, target, id }]).then((item) => { + const hit = item[0] + if (hit) return hit + throw new Error("Failed to touch plugin metadata.") + }) +} + +export async function setTheme(id: string, name: string, theme: Theme): Promise { + const file = storePath() + await Flock.withLock(lock(file), async () => { + const store = await read(file) + const entry = store[id] + if (!entry) return + entry.themes = { + ...entry.themes, + [name]: theme, + } + await Filesystem.writeJson(file, store) + }) +} + +export async function list(): Promise { + const file = storePath() + return Flock.withLock(lock(file), async () => read(file)) +} + +export * as PluginMeta from "./meta" From 06d247c70982b9bba0bb25f4b990fb59e3374650 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:02:08 -0400 Subject: [PATCH 025/335] refactor: unwrap FileIgnore namespace + self-reexport (#22937) --- packages/opencode/src/file/ignore.ts | 140 +++++++++++++-------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/packages/opencode/src/file/ignore.ts b/packages/opencode/src/file/ignore.ts index 63f2f594eb..efce872808 100644 --- a/packages/opencode/src/file/ignore.ts +++ b/packages/opencode/src/file/ignore.ts @@ -1,81 +1,81 @@ import { Glob } from "@opencode-ai/shared/util/glob" -export namespace FileIgnore { - const FOLDERS = new Set([ - "node_modules", - "bower_components", - ".pnpm-store", - "vendor", - ".npm", - "dist", - "build", - "out", - ".next", - "target", - "bin", - "obj", - ".git", - ".svn", - ".hg", - ".vscode", - ".idea", - ".turbo", - ".output", - "desktop", - ".sst", - ".cache", - ".webkit-cache", - "__pycache__", - ".pytest_cache", - "mypy_cache", - ".history", - ".gradle", - ]) +const FOLDERS = new Set([ + "node_modules", + "bower_components", + ".pnpm-store", + "vendor", + ".npm", + "dist", + "build", + "out", + ".next", + "target", + "bin", + "obj", + ".git", + ".svn", + ".hg", + ".vscode", + ".idea", + ".turbo", + ".output", + "desktop", + ".sst", + ".cache", + ".webkit-cache", + "__pycache__", + ".pytest_cache", + "mypy_cache", + ".history", + ".gradle", +]) - const FILES = [ - "**/*.swp", - "**/*.swo", +const FILES = [ + "**/*.swp", + "**/*.swo", - "**/*.pyc", + "**/*.pyc", - // OS - "**/.DS_Store", - "**/Thumbs.db", + // OS + "**/.DS_Store", + "**/Thumbs.db", - // Logs & temp - "**/logs/**", - "**/tmp/**", - "**/temp/**", - "**/*.log", + // Logs & temp + "**/logs/**", + "**/tmp/**", + "**/temp/**", + "**/*.log", - // Coverage/test outputs - "**/coverage/**", - "**/.nyc_output/**", - ] + // Coverage/test outputs + "**/coverage/**", + "**/.nyc_output/**", +] - export const PATTERNS = [...FILES, ...FOLDERS] +export const PATTERNS = [...FILES, ...FOLDERS] - export function match( - filepath: string, - opts?: { - extra?: string[] - whitelist?: string[] - }, - ) { - for (const pattern of opts?.whitelist || []) { - if (Glob.match(pattern, filepath)) return false - } - - const parts = filepath.split(/[/\\]/) - for (let i = 0; i < parts.length; i++) { - if (FOLDERS.has(parts[i])) return true - } - - const extra = opts?.extra || [] - for (const pattern of [...FILES, ...extra]) { - if (Glob.match(pattern, filepath)) return true - } - - return false +export function match( + filepath: string, + opts?: { + extra?: string[] + whitelist?: string[] + }, +) { + for (const pattern of opts?.whitelist || []) { + if (Glob.match(pattern, filepath)) return false } + + const parts = filepath.split(/[/\\]/) + for (let i = 0; i < parts.length; i++) { + if (FOLDERS.has(parts[i])) return true + } + + const extra = opts?.extra || [] + for (const pattern of [...FILES, ...extra]) { + if (Glob.match(pattern, filepath)) return true + } + + return false } + +export * as FileIgnore from "./ignore" From 2704ad9110e6705dacd1b018a7245c1950a3ae80 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:02:24 -0400 Subject: [PATCH 026/335] refactor: unwrap TuiConfig namespace + self-reexport (#22952) --- .../opencode/src/cli/cmd/tui/config/tui.ts | 360 +++++++++--------- 1 file changed, 180 insertions(+), 180 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index e8eb9ff5d3..d264273bca 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -17,197 +17,197 @@ import { InstallationLocal, InstallationVersion } from "@/installation/version" import { makeRuntime } from "@/cli/effect/runtime" import { Filesystem, Log } from "@/util" -export namespace TuiConfig { - const log = Log.create({ service: "tui.config" }) +const log = Log.create({ service: "tui.config" }) - export const Info = TuiInfo +export const Info = TuiInfo - type Acc = { - result: Info - } +type Acc = { + result: Info +} - type State = { - config: Info - deps: Array> - } +type State = { + config: Info + deps: Array> +} - export type Info = z.output & { - // Internal resolved plugin list used by runtime loading. - plugin_origins?: ConfigPlugin.Origin[] - } +export type Info = z.output & { + // Internal resolved plugin list used by runtime loading. + plugin_origins?: ConfigPlugin.Origin[] +} - export interface Interface { - readonly get: () => Effect.Effect - readonly waitForDependencies: () => Effect.Effect - } +export interface Interface { + readonly get: () => Effect.Effect + readonly waitForDependencies: () => Effect.Effect +} - export class Service extends Context.Service()("@opencode/TuiConfig") {} +export class Service extends Context.Service()("@opencode/TuiConfig") {} - function pluginScope(file: string, ctx: { directory: string }): ConfigPlugin.Scope { - if (Filesystem.contains(ctx.directory, file)) return "local" - // if (ctx.worktree !== "/" && Filesystem.contains(ctx.worktree, file)) return "local" - return "global" - } +function pluginScope(file: string, ctx: { directory: string }): ConfigPlugin.Scope { + if (Filesystem.contains(ctx.directory, file)) return "local" + // if (ctx.worktree !== "/" && Filesystem.contains(ctx.worktree, file)) return "local" + return "global" +} - function customPath() { - return Flag.OPENCODE_TUI_CONFIG - } +function customPath() { + return Flag.OPENCODE_TUI_CONFIG +} - function normalize(raw: Record) { - const data = { ...raw } - if (!("tui" in data)) return data - if (!isRecord(data.tui)) { - delete data.tui - return data - } - - const tui = data.tui +function normalize(raw: Record) { + const data = { ...raw } + if (!("tui" in data)) return data + if (!isRecord(data.tui)) { delete data.tui - return { - ...tui, - ...data, - } + return data } - async function resolvePlugins(config: Info, configFilepath: string) { - if (!config.plugin) return config - for (let i = 0; i < config.plugin.length; i++) { - config.plugin[i] = await ConfigPlugin.resolvePluginSpec(config.plugin[i], configFilepath) - } - return config - } - - async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) { - const data = await loadFile(file) - acc.result = mergeDeep(acc.result, data) - if (!data.plugin?.length) return - - const scope = pluginScope(file, ctx) - const plugins = ConfigPlugin.deduplicatePluginOrigins([ - ...(acc.result.plugin_origins ?? []), - ...data.plugin.map((spec) => ({ spec, scope, source: file })), - ]) - acc.result.plugin = plugins.map((item) => item.spec) - acc.result.plugin_origins = plugins - } - - async function loadState(ctx: { directory: string }) { - let projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("tui", ctx.directory) - const directories = await ConfigPaths.directories(ctx.directory) - const custom = customPath() - await migrateTuiConfig({ directories, custom, cwd: ctx.directory }) - // Re-compute after migration since migrateTuiConfig may have created new tui.json files - projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("tui", ctx.directory) - - const acc: Acc = { - result: {}, - } - - for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) { - await mergeFile(acc, file, ctx) - } - - if (custom) { - await mergeFile(acc, custom, ctx) - log.debug("loaded custom tui config", { path: custom }) - } - - for (const file of projectFiles) { - await mergeFile(acc, file, ctx) - } - - const dirs = unique(directories).filter((dir) => dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) - - for (const dir of dirs) { - if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue - for (const file of ConfigPaths.fileInDirectory(dir, "tui")) { - await mergeFile(acc, file, ctx) - } - } - - const keybinds = { ...(acc.result.keybinds ?? {}) } - if (process.platform === "win32") { - // Native Windows terminals do not support POSIX suspend, so prefer prompt undo. - keybinds.terminal_suspend = "none" - keybinds.input_undo ??= unique([ - "ctrl+z", - ...ConfigKeybinds.Keybinds.shape.input_undo.parse(undefined).split(","), - ]).join(",") - } - acc.result.keybinds = ConfigKeybinds.Keybinds.parse(keybinds) - - return { - config: acc.result, - dirs: acc.result.plugin?.length ? dirs : [], - } - } - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const directory = yield* CurrentWorkingDirectory - const npm = yield* Npm.Service - const data = yield* Effect.promise(() => loadState({ directory })) - const deps = yield* Effect.forEach( - data.dirs, - (dir) => - npm - .install(dir, { - add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)], - }) - .pipe(Effect.forkScoped), - { - concurrency: "unbounded", - }, - ) - - const get = Effect.fn("TuiConfig.get")(() => Effect.succeed(data.config)) - - const waitForDependencies = Effect.fn("TuiConfig.waitForDependencies")(() => - Effect.forEach(deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.ignore(), Effect.asVoid), - ) - return Service.of({ get, waitForDependencies }) - }).pipe(Effect.withSpan("TuiConfig.layer")), - ) - - export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer)) - - const { runPromise } = makeRuntime(Service, defaultLayer) - - export async function waitForDependencies() { - await runPromise((svc) => svc.waitForDependencies()) - } - - export async function get() { - return runPromise((svc) => svc.get()) - } - - async function loadFile(filepath: string): Promise { - const text = await ConfigPaths.readFile(filepath) - if (!text) return {} - return load(text, filepath).catch((error) => { - log.warn("failed to load tui config", { path: filepath, error }) - return {} - }) - } - - async function load(text: string, configFilepath: string): Promise { - return ConfigParse.load(Info, text, { - type: "path", - path: configFilepath, - missing: "empty", - normalize: (data) => { - if (!isRecord(data)) return {} - - // Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json - // (mirroring the old opencode.json shape) still get their settings applied. - return normalize(data) - }, - }) - .then((data) => resolvePlugins(data, configFilepath)) - .catch((error) => { - log.warn("invalid tui config", { path: configFilepath, error }) - return {} - }) + const tui = data.tui + delete data.tui + return { + ...tui, + ...data, } } + +async function resolvePlugins(config: Info, configFilepath: string) { + if (!config.plugin) return config + for (let i = 0; i < config.plugin.length; i++) { + config.plugin[i] = await ConfigPlugin.resolvePluginSpec(config.plugin[i], configFilepath) + } + return config +} + +async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) { + const data = await loadFile(file) + acc.result = mergeDeep(acc.result, data) + if (!data.plugin?.length) return + + const scope = pluginScope(file, ctx) + const plugins = ConfigPlugin.deduplicatePluginOrigins([ + ...(acc.result.plugin_origins ?? []), + ...data.plugin.map((spec) => ({ spec, scope, source: file })), + ]) + acc.result.plugin = plugins.map((item) => item.spec) + acc.result.plugin_origins = plugins +} + +async function loadState(ctx: { directory: string }) { + let projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("tui", ctx.directory) + const directories = await ConfigPaths.directories(ctx.directory) + const custom = customPath() + await migrateTuiConfig({ directories, custom, cwd: ctx.directory }) + // Re-compute after migration since migrateTuiConfig may have created new tui.json files + projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("tui", ctx.directory) + + const acc: Acc = { + result: {}, + } + + for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) { + await mergeFile(acc, file, ctx) + } + + if (custom) { + await mergeFile(acc, custom, ctx) + log.debug("loaded custom tui config", { path: custom }) + } + + for (const file of projectFiles) { + await mergeFile(acc, file, ctx) + } + + const dirs = unique(directories).filter((dir) => dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) + + for (const dir of dirs) { + if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue + for (const file of ConfigPaths.fileInDirectory(dir, "tui")) { + await mergeFile(acc, file, ctx) + } + } + + const keybinds = { ...(acc.result.keybinds ?? {}) } + if (process.platform === "win32") { + // Native Windows terminals do not support POSIX suspend, so prefer prompt undo. + keybinds.terminal_suspend = "none" + keybinds.input_undo ??= unique([ + "ctrl+z", + ...ConfigKeybinds.Keybinds.shape.input_undo.parse(undefined).split(","), + ]).join(",") + } + acc.result.keybinds = ConfigKeybinds.Keybinds.parse(keybinds) + + return { + config: acc.result, + dirs: acc.result.plugin?.length ? dirs : [], + } +} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const directory = yield* CurrentWorkingDirectory + const npm = yield* Npm.Service + const data = yield* Effect.promise(() => loadState({ directory })) + const deps = yield* Effect.forEach( + data.dirs, + (dir) => + npm + .install(dir, { + add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)], + }) + .pipe(Effect.forkScoped), + { + concurrency: "unbounded", + }, + ) + + const get = Effect.fn("TuiConfig.get")(() => Effect.succeed(data.config)) + + const waitForDependencies = Effect.fn("TuiConfig.waitForDependencies")(() => + Effect.forEach(deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.ignore(), Effect.asVoid), + ) + return Service.of({ get, waitForDependencies }) + }).pipe(Effect.withSpan("TuiConfig.layer")), +) + +export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer)) + +const { runPromise } = makeRuntime(Service, defaultLayer) + +export async function waitForDependencies() { + await runPromise((svc) => svc.waitForDependencies()) +} + +export async function get() { + return runPromise((svc) => svc.get()) +} + +async function loadFile(filepath: string): Promise { + const text = await ConfigPaths.readFile(filepath) + if (!text) return {} + return load(text, filepath).catch((error) => { + log.warn("failed to load tui config", { path: filepath, error }) + return {} + }) +} + +async function load(text: string, configFilepath: string): Promise { + return ConfigParse.load(Info, text, { + type: "path", + path: configFilepath, + missing: "empty", + normalize: (data) => { + if (!isRecord(data)) return {} + + // Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json + // (mirroring the old opencode.json shape) still get their settings applied. + return normalize(data) + }, + }) + .then((data) => resolvePlugins(data, configFilepath)) + .catch((error) => { + log.warn("invalid tui config", { path: configFilepath, error }) + return {} + }) +} + +export * as TuiConfig from "./tui" From 059b32c2124da9bb7a9e3cc1e9a49e3a72f29740 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:02:51 -0400 Subject: [PATCH 027/335] refactor: unwrap Protected namespace + self-reexport (#22938) --- packages/opencode/src/file/protected.ts | 38 ++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/opencode/src/file/protected.ts b/packages/opencode/src/file/protected.ts index d519746193..a316e790b8 100644 --- a/packages/opencode/src/file/protected.ts +++ b/packages/opencode/src/file/protected.ts @@ -37,23 +37,23 @@ const DARWIN_ROOT = ["/.DocumentRevisions-V100", "/.Spotlight-V100", "/.Trashes" const WIN32_HOME = ["AppData", "Downloads", "Desktop", "Documents", "Pictures", "Music", "Videos", "OneDrive"] -export namespace Protected { - /** Directory basenames to skip when scanning the home directory. */ - export function names(): ReadonlySet { - if (process.platform === "darwin") return new Set(DARWIN_HOME) - if (process.platform === "win32") return new Set(WIN32_HOME) - return new Set() - } - - /** Absolute paths that should never be watched, stated, or scanned. */ - export function paths(): string[] { - if (process.platform === "darwin") - return [ - ...DARWIN_HOME.map((n) => path.join(home, n)), - ...DARWIN_LIBRARY.map((n) => path.join(home, "Library", n)), - ...DARWIN_ROOT, - ] - if (process.platform === "win32") return WIN32_HOME.map((n) => path.join(home, n)) - return [] - } +/** Directory basenames to skip when scanning the home directory. */ +export function names(): ReadonlySet { + if (process.platform === "darwin") return new Set(DARWIN_HOME) + if (process.platform === "win32") return new Set(WIN32_HOME) + return new Set() } + +/** Absolute paths that should never be watched, stated, or scanned. */ +export function paths(): string[] { + if (process.platform === "darwin") + return [ + ...DARWIN_HOME.map((n) => path.join(home, n)), + ...DARWIN_LIBRARY.map((n) => path.join(home, "Library", n)), + ...DARWIN_ROOT, + ] + if (process.platform === "win32") return WIN32_HOME.map((n) => path.join(home, n)) + return [] +} + +export * as Protected from "./protected" From 635970b0a117481603d9894975ca502bc3887224 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:02:53 -0400 Subject: [PATCH 028/335] refactor: unwrap ConfigSkills namespace + self-reexport (#22950) --- packages/opencode/src/config/skills.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/config/skills.ts b/packages/opencode/src/config/skills.ts index bdc63f5d6a..38cbf99e7d 100644 --- a/packages/opencode/src/config/skills.ts +++ b/packages/opencode/src/config/skills.ts @@ -1,13 +1,13 @@ import z from "zod" -export namespace ConfigSkills { - export const Info = z.object({ - paths: z.array(z.string()).optional().describe("Additional paths to skill folders"), - urls: z - .array(z.string()) - .optional() - .describe("URLs to fetch skills from (e.g., https://example.com/.well-known/skills/)"), - }) +export const Info = z.object({ + paths: z.array(z.string()).optional().describe("Additional paths to skill folders"), + urls: z + .array(z.string()) + .optional() + .describe("URLs to fetch skills from (e.g., https://example.com/.well-known/skills/)"), +}) - export type Info = z.infer -} +export type Info = z.infer + +export * as ConfigSkills from "./skills" From 53dc7b164940edbc5793bac83f91d7fca7b78fe5 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 00:04:01 +0000 Subject: [PATCH 029/335] chore: generate --- packages/opencode/src/acp/agent.ts | 10 +++------- packages/opencode/src/agent/agent.ts | 3 +-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 7180feabcb..f12328153b 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -1162,8 +1162,7 @@ export class Agent implements ACPAgent { (await (async () => { if (!availableModes.length) return undefined const defaultAgentName = await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent())) - const resolvedModeId = - availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id + const resolvedModeId = availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id this.sessionManager.setMode(sessionId, resolvedModeId) return resolvedModeId })()) @@ -1362,8 +1361,7 @@ export class Agent implements ACPAgent { if (!current) { this.sessionManager.setModel(session.id, model) } - const agent = - session.modeId ?? (await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent()))) + const agent = session.modeId ?? (await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent()))) const parts: Array< | { type: "text"; text: string; synthetic?: boolean; ignored?: boolean } @@ -1729,9 +1727,7 @@ function buildAvailableModels( ): ModelOption[] { const includeVariants = options.includeVariants ?? false return providers.flatMap((provider) => { - const unsorted: Array<{ id: string; name: string; variants?: Record }> = Object.values( - provider.models, - ) + const unsorted: Array<{ id: string; name: string; variants?: Record }> = Object.values(provider.models) const models = Provider.sort(unsorted) return models.flatMap((model) => { const base: ModelOption = { diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 07f742fe12..355718b6bf 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -136,8 +136,7 @@ export const layer = Layer.effect( edit: { "*": "deny", [path.join(".opencode", "plans", "*.md")]: "allow", - [path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: - "allow", + [path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: "allow", }, }), user, From c0bfccc15ea6e2baea1d5b67f73d689317caa2af Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:11:17 -0400 Subject: [PATCH 030/335] tooling: add unwrap-and-self-reexport + batch-unwrap-pr scripts (#22929) --- packages/opencode/script/batch-unwrap-pr.ts | 230 +++++++++++++++++ .../script/unwrap-and-self-reexport.ts | 241 ++++++++++++++++++ 2 files changed, 471 insertions(+) create mode 100644 packages/opencode/script/batch-unwrap-pr.ts create mode 100644 packages/opencode/script/unwrap-and-self-reexport.ts diff --git a/packages/opencode/script/batch-unwrap-pr.ts b/packages/opencode/script/batch-unwrap-pr.ts new file mode 100644 index 0000000000..5730501412 --- /dev/null +++ b/packages/opencode/script/batch-unwrap-pr.ts @@ -0,0 +1,230 @@ +#!/usr/bin/env bun +/** + * Automate the full per-file namespace→self-reexport migration: + * + * 1. Create a worktree at ../opencode-worktrees/ns- on a new branch + * `kit/ns-` off `origin/dev`. + * 2. Symlink `node_modules` from the main repo into the worktree root so + * builds work without a fresh `bun install`. + * 3. Run `script/unwrap-and-self-reexport.ts` on the target file inside the worktree. + * 4. Verify: + * - `bunx --bun tsgo --noEmit` (pre-existing plugin.ts cross-worktree + * noise ignored — we compare against a pre-change baseline captured + * via `git stash`, so only NEW errors fail). + * - `bun run --conditions=browser ./src/index.ts generate`. + * - Relevant tests under `test/` if that directory exists. + * 5. Commit, push with `--no-verify`, and open a PR titled after the + * namespace. + * + * Usage: + * + * bun script/batch-unwrap-pr.ts src/file/ignore.ts + * bun script/batch-unwrap-pr.ts src/file/ignore.ts src/file/watcher.ts # multiple + * bun script/batch-unwrap-pr.ts --dry-run src/file/ignore.ts # plan only + * + * Repo assumptions: + * + * - Main checkout at /Users/kit/code/open-source/opencode (configurable via + * --repo-root=...). + * - Worktree root at /Users/kit/code/open-source/opencode-worktrees + * (configurable via --worktree-root=...). + * + * The script does NOT enable auto-merge; that's a separate manual step if we + * want it. + */ + +import fs from "node:fs" +import path from "node:path" +import { spawnSync, type SpawnSyncReturns } from "node:child_process" + +type Cmd = string[] + +function run( + cwd: string, + cmd: Cmd, + opts: { capture?: boolean; allowFail?: boolean; stdin?: string } = {}, +): SpawnSyncReturns { + const result = spawnSync(cmd[0], cmd.slice(1), { + cwd, + stdio: opts.capture ? ["pipe", "pipe", "pipe"] : ["inherit", "inherit", "inherit"], + encoding: "utf-8", + input: opts.stdin, + }) + if (!opts.allowFail && result.status !== 0) { + const label = `${path.basename(cmd[0])} ${cmd.slice(1).join(" ")}` + console.error(`[fail] ${label} (cwd=${cwd})`) + if (opts.capture) { + if (result.stdout) console.error(result.stdout) + if (result.stderr) console.error(result.stderr) + } + process.exit(result.status ?? 1) + } + return result +} + +function fileSlug(fileArg: string): string { + // src/file/ignore.ts → file-ignore + return fileArg + .replace(/^src\//, "") + .replace(/\.tsx?$/, "") + .replace(/[\/_]/g, "-") +} + +function readNamespace(absFile: string): string { + const content = fs.readFileSync(absFile, "utf-8") + const match = content.match(/^export\s+namespace\s+(\w+)\s*\{/m) + if (!match) { + console.error(`no \`export namespace\` found in ${absFile}`) + process.exit(1) + } + return match[1] +} + +// --------------------------------------------------------------------------- + +const args = process.argv.slice(2) +const dryRun = args.includes("--dry-run") +const repoRoot = ( + args.find((a) => a.startsWith("--repo-root=")) ?? "--repo-root=/Users/kit/code/open-source/opencode" +).split("=")[1] +const worktreeRoot = ( + args.find((a) => a.startsWith("--worktree-root=")) ?? "--worktree-root=/Users/kit/code/open-source/opencode-worktrees" +).split("=")[1] +const targets = args.filter((a) => !a.startsWith("--")) + +if (targets.length === 0) { + console.error("Usage: bun script/batch-unwrap-pr.ts [more files...] [--dry-run]") + process.exit(1) +} + +if (!fs.existsSync(worktreeRoot)) fs.mkdirSync(worktreeRoot, { recursive: true }) + +for (const rel of targets) { + const absSrc = path.join(repoRoot, "packages", "opencode", rel) + if (!fs.existsSync(absSrc)) { + console.error(`skip ${rel}: file does not exist under ${repoRoot}/packages/opencode`) + continue + } + const slug = fileSlug(rel) + const branch = `kit/ns-${slug}` + const wt = path.join(worktreeRoot, `ns-${slug}`) + const ns = readNamespace(absSrc) + + console.log(`\n=== ${rel} → ${ns} (branch=${branch} wt=${path.basename(wt)}) ===`) + + if (dryRun) { + console.log(` would create worktree ${wt}`) + console.log(` would run unwrap on packages/opencode/${rel}`) + console.log(` would commit, push, and open PR`) + continue + } + + // Sync dev (fetch only; we branch off origin/dev directly). + run(repoRoot, ["git", "fetch", "origin", "dev", "--quiet"]) + + // Create worktree + branch. + if (fs.existsSync(wt)) { + console.log(` worktree already exists at ${wt}; skipping`) + continue + } + run(repoRoot, ["git", "worktree", "add", "-b", branch, wt, "origin/dev"]) + + // Symlink node_modules so bun/tsgo work without a full install. + // We link both the repo root and packages/opencode, since the opencode + // package has its own local node_modules (including bunfig.toml preload deps + // like @opentui/solid) that aren't hoisted to the root. + const wtRootNodeModules = path.join(wt, "node_modules") + if (!fs.existsSync(wtRootNodeModules)) { + fs.symlinkSync(path.join(repoRoot, "node_modules"), wtRootNodeModules) + } + const wtOpencode = path.join(wt, "packages", "opencode") + const wtOpencodeNodeModules = path.join(wtOpencode, "node_modules") + if (!fs.existsSync(wtOpencodeNodeModules)) { + fs.symlinkSync(path.join(repoRoot, "packages", "opencode", "node_modules"), wtOpencodeNodeModules) + } + const wtTarget = path.join(wt, "packages", "opencode", rel) + + // Baseline tsgo output (pre-change). + const baselinePath = path.join(wt, ".ns-baseline.txt") + const baseline = run(wtOpencode, ["bunx", "--bun", "tsgo", "--noEmit"], { capture: true, allowFail: true }) + fs.writeFileSync(baselinePath, (baseline.stdout ?? "") + (baseline.stderr ?? "")) + + // Run the unwrap script from the MAIN repo checkout (where the tooling + // lives) targeting the worktree's file by absolute path. We run from the + // worktree root (not `packages/opencode`) to avoid triggering the + // bunfig.toml preload, which needs `@opentui/solid` that only the TUI + // workspace has installed. + const unwrapScript = path.join(repoRoot, "packages", "opencode", "script", "unwrap-and-self-reexport.ts") + run(wt, ["bun", unwrapScript, wtTarget]) + + // Post-change tsgo. + const after = run(wtOpencode, ["bunx", "--bun", "tsgo", "--noEmit"], { capture: true, allowFail: true }) + const afterText = (after.stdout ?? "") + (after.stderr ?? "") + + // Compare line-sets to detect NEW tsgo errors. + const sanitize = (s: string) => + s + .split("\n") + .map((l) => l.replace(/\s+$/, "")) + .filter(Boolean) + .sort() + .join("\n") + const baselineSorted = sanitize(fs.readFileSync(baselinePath, "utf-8")) + const afterSorted = sanitize(afterText) + if (baselineSorted !== afterSorted) { + console.log(` tsgo output differs from baseline. Showing diff:`) + const diffResult = spawnSync("diff", ["-u", baselinePath, "-"], { input: afterText, encoding: "utf-8" }) + if (diffResult.stdout) console.log(diffResult.stdout) + if (diffResult.stderr) console.log(diffResult.stderr) + console.error(` aborting ${rel}; investigate manually in ${wt}`) + process.exit(1) + } + + // SDK build. + run(wtOpencode, ["bun", "run", "--conditions=browser", "./src/index.ts", "generate"], { capture: true }) + + // Run tests for the directory, if a matching test dir exists. + const dirName = path.basename(path.dirname(rel)) + const testDir = path.join(wt, "packages", "opencode", "test", dirName) + if (fs.existsSync(testDir)) { + const testResult = run(wtOpencode, ["bun", "run", "test", `test/${dirName}`], { capture: true, allowFail: true }) + const combined = (testResult.stdout ?? "") + (testResult.stderr ?? "") + if (testResult.status !== 0) { + console.error(combined) + console.error(` tests failed for ${rel}; aborting`) + process.exit(1) + } + // Surface the summary line if present. + const summary = combined + .split("\n") + .filter((l) => /\bpass\b|\bfail\b/.test(l)) + .slice(-3) + .join("\n") + if (summary) console.log(` tests: ${summary.replace(/\n/g, " | ")}`) + } else { + console.log(` tests: no test/${dirName} directory, skipping`) + } + + // Clean up baseline file before committing. + fs.unlinkSync(baselinePath) + + // Commit, push, open PR. + const commitMsg = `refactor: unwrap ${ns} namespace + self-reexport` + run(wt, ["git", "add", "-A"]) + run(wt, ["git", "commit", "-m", commitMsg]) + run(wt, ["git", "push", "-u", "origin", branch, "--no-verify"]) + + const prBody = [ + "## Summary", + `- Unwrap the \`${ns}\` namespace in \`packages/opencode/${rel}\` to flat top-level exports.`, + `- Append \`export * as ${ns} from "./${path.basename(rel, ".ts")}"\` so consumers keep the same \`${ns}.x\` import ergonomics.`, + "", + "## Verification (local)", + "- `bunx --bun tsgo --noEmit` — no new errors vs baseline.", + "- `bun run --conditions=browser ./src/index.ts generate` — clean.", + `- \`bun run test test/${dirName}\` — all pass (if applicable).`, + ].join("\n") + run(wt, ["gh", "pr", "create", "--title", commitMsg, "--base", "dev", "--body", prBody]) + + console.log(` PR opened for ${rel}`) +} diff --git a/packages/opencode/script/unwrap-and-self-reexport.ts b/packages/opencode/script/unwrap-and-self-reexport.ts new file mode 100644 index 0000000000..5ae703182e --- /dev/null +++ b/packages/opencode/script/unwrap-and-self-reexport.ts @@ -0,0 +1,241 @@ +#!/usr/bin/env bun +/** + * Unwrap a single `export namespace` in a file into flat top-level exports + * plus a self-reexport at the bottom of the same file. + * + * Usage: + * + * bun script/unwrap-and-self-reexport.ts src/file/ignore.ts + * bun script/unwrap-and-self-reexport.ts src/file/ignore.ts --dry-run + * + * Input file shape: + * + * // imports ... + * + * export namespace FileIgnore { + * export function ...(...) { ... } + * const helper = ... + * } + * + * Output shape: + * + * // imports ... + * + * export function ...(...) { ... } + * const helper = ... + * + * export * as FileIgnore from "./ignore" + * + * What the script does: + * + * 1. Uses ast-grep to locate the single `export namespace Foo { ... }` block. + * 2. Removes the `export namespace Foo {` line and the matching closing `}`. + * 3. Dedents the body by one indent level (2 spaces). + * 4. Rewrites `Foo.Bar` self-references inside the file to just `Bar` + * (but only for names that are actually exported from the namespace — + * non-exported members get the same treatment so references remain valid). + * 5. Appends `export * as Foo from "./"` at the end of the file. + * + * What it does NOT do: + * + * - Does not create or modify barrel `index.ts` files. + * - Does not rewrite any consumer imports. Consumers already import from + * the file path itself (e.g. `import { FileIgnore } from "../file/ignore"`); + * the self-reexport keeps that import working unchanged. + * - Does not handle files with more than one `export namespace` declaration. + * The script refuses that case. + * + * Requires: ast-grep (`brew install ast-grep`). + */ + +import fs from "node:fs" +import path from "node:path" + +const args = process.argv.slice(2) +const dryRun = args.includes("--dry-run") +const targetArg = args.find((a) => !a.startsWith("--")) + +if (!targetArg) { + console.error("Usage: bun script/unwrap-and-self-reexport.ts [--dry-run]") + process.exit(1) +} + +const absPath = path.resolve(targetArg) +if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) { + console.error(`Not a file: ${absPath}`) + process.exit(1) +} + +// Locate the namespace block with ast-grep (accurate AST boundaries). +const ast = Bun.spawnSync( + ["ast-grep", "run", "--pattern", "export namespace $NAME { $$$BODY }", "--lang", "typescript", "--json", absPath], + { stdout: "pipe", stderr: "pipe" }, +) +if (ast.exitCode !== 0) { + console.error("ast-grep failed:", ast.stderr.toString()) + process.exit(1) +} + +type AstMatch = { + range: { start: { line: number; column: number }; end: { line: number; column: number } } + metaVariables: { single: Record } +} +const matches = JSON.parse(ast.stdout.toString()) as AstMatch[] +if (matches.length === 0) { + console.error(`No \`export namespace\` found in ${path.relative(process.cwd(), absPath)}`) + process.exit(1) +} +if (matches.length > 1) { + console.error(`File has ${matches.length} \`export namespace\` declarations — this script handles one per file.`) + for (const m of matches) console.error(` ${m.metaVariables.single.NAME.text} (line ${m.range.start.line + 1})`) + process.exit(1) +} + +const match = matches[0] +const nsName = match.metaVariables.single.NAME.text +const startLine = match.range.start.line +const endLine = match.range.end.line + +const original = fs.readFileSync(absPath, "utf-8") +const lines = original.split("\n") + +// Split the file into before/body/after. +const before = lines.slice(0, startLine) +const body = lines.slice(startLine + 1, endLine) +const after = lines.slice(endLine + 1) + +// Dedent body by one indent level (2 spaces). +const dedented = body.map((line) => { + if (line === "") return "" + if (line.startsWith(" ")) return line.slice(2) + return line +}) + +// Collect all top-level declared identifiers inside the namespace body so we can +// rewrite `Foo.X` → `X` when X is one of them. We gather BOTH exported and +// non-exported names because the namespace body might reference its own +// non-exported helpers via `Foo.helper` too. +const declaredNames = new Set() +const declRe = + /^\s*(?:export\s+)?(?:abstract\s+)?(?:async\s+)?(?:const|let|var|function|class|interface|type|enum)\s+(\w+)/ +for (const line of dedented) { + const m = line.match(declRe) + if (m) declaredNames.add(m[1]) +} +// Also capture `export { X, Y }` re-exports inside the namespace. +const reExportRe = /export\s*\{\s*([^}]+)\}/g +for (const line of dedented) { + for (const reExport of line.matchAll(reExportRe)) { + for (const part of reExport[1].split(",")) { + const name = part + .trim() + .split(/\s+as\s+/) + .pop()! + .trim() + if (name) declaredNames.add(name) + } + } +} + +// Rewrite `Foo.X` → `X` inside the body, avoiding matches in strings, comments, +// templates. We walk the line char-by-char rather than using a regex so we can +// skip over those segments cleanly. +let rewriteCount = 0 +function rewriteLine(line: string): string { + const out: string[] = [] + let i = 0 + let stringQuote: string | null = null + while (i < line.length) { + const ch = line[i] + // String / template literal pass-through. + if (stringQuote) { + out.push(ch) + if (ch === "\\" && i + 1 < line.length) { + out.push(line[i + 1]) + i += 2 + continue + } + if (ch === stringQuote) stringQuote = null + i++ + continue + } + if (ch === '"' || ch === "'" || ch === "`") { + stringQuote = ch + out.push(ch) + i++ + continue + } + // Line comment: emit the rest of the line untouched. + if (ch === "/" && line[i + 1] === "/") { + out.push(line.slice(i)) + i = line.length + continue + } + // Block comment: emit until "*/" if present on same line; else rest of line. + if (ch === "/" && line[i + 1] === "*") { + const end = line.indexOf("*/", i + 2) + if (end === -1) { + out.push(line.slice(i)) + i = line.length + } else { + out.push(line.slice(i, end + 2)) + i = end + 2 + } + continue + } + // Try to match `Foo.` at this position. + if (line.startsWith(nsName + ".", i)) { + // Make sure the char before is NOT a word character (otherwise we'd be in the middle of another identifier). + const prev = i === 0 ? "" : line[i - 1] + if (!/\w/.test(prev)) { + const after = line.slice(i + nsName.length + 1) + const nameMatch = after.match(/^([A-Za-z_$][\w$]*)/) + if (nameMatch && declaredNames.has(nameMatch[1])) { + out.push(nameMatch[1]) + i += nsName.length + 1 + nameMatch[1].length + rewriteCount++ + continue + } + } + } + out.push(ch) + i++ + } + return out.join("") +} +const rewrittenBody = dedented.map(rewriteLine) + +// Assemble the new file. Collapse multiple trailing blank lines so the +// self-reexport sits cleanly at the end. +const basename = path.basename(absPath, ".ts") +const assembled = [...before, ...rewrittenBody, ...after].join("\n") +const trimmed = assembled.replace(/\s+$/g, "") +const output = `${trimmed}\n\nexport * as ${nsName} from "./${basename}"\n` + +if (dryRun) { + console.log(`--- dry run: ${path.relative(process.cwd(), absPath)} ---`) + console.log(`namespace: ${nsName}`) + console.log(`body lines: ${body.length}`) + console.log(`declared names: ${Array.from(declaredNames).join(", ") || "(none)"}`) + console.log(`self-refs rewr: ${rewriteCount}`) + console.log(`self-reexport: export * as ${nsName} from "./${basename}"`) + console.log(`output preview (last 10 lines):`) + const outputLines = output.split("\n") + for (const l of outputLines.slice(Math.max(0, outputLines.length - 10))) { + console.log(` ${l}`) + } + process.exit(0) +} + +fs.writeFileSync(absPath, output) +console.log(`unwrapped ${path.relative(process.cwd(), absPath)} → ${nsName}`) +console.log(` body lines: ${body.length}`) +console.log(` self-refs rewr: ${rewriteCount}`) +console.log(` self-reexport: export * as ${nsName} from "./${basename}"`) +console.log("") +console.log("Next: verify with") +console.log(" bunx --bun tsgo --noEmit") +console.log(" bun run --conditions=browser ./src/index.ts generate") +console.log( + ` bun run test test/${path.relative(path.join(path.dirname(absPath), "..", ".."), absPath).replace(/\.ts$/, "")}*`, +) From 54078c4caea1adadea25ca1c4ec1479f3ab4e423 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:11:19 -0400 Subject: [PATCH 031/335] refactor: unwrap Shell namespace + self-reexport (#22964) --- packages/opencode/src/shell/shell.ts | 186 +++++++++++++-------------- 1 file changed, 93 insertions(+), 93 deletions(-) diff --git a/packages/opencode/src/shell/shell.ts b/packages/opencode/src/shell/shell.ts index 056a794dc8..60643c10b0 100644 --- a/packages/opencode/src/shell/shell.ts +++ b/packages/opencode/src/shell/shell.ts @@ -8,103 +8,103 @@ import { setTimeout as sleep } from "node:timers/promises" const SIGKILL_TIMEOUT_MS = 200 -export namespace Shell { - const BLACKLIST = new Set(["fish", "nu"]) - const LOGIN = new Set(["bash", "dash", "fish", "ksh", "sh", "zsh"]) - const POSIX = new Set(["bash", "dash", "ksh", "sh", "zsh"]) +const BLACKLIST = new Set(["fish", "nu"]) +const LOGIN = new Set(["bash", "dash", "fish", "ksh", "sh", "zsh"]) +const POSIX = new Set(["bash", "dash", "ksh", "sh", "zsh"]) - export async function killTree(proc: ChildProcess, opts?: { exited?: () => boolean }): Promise { - const pid = proc.pid - if (!pid || opts?.exited?.()) return +export async function killTree(proc: ChildProcess, opts?: { exited?: () => boolean }): Promise { + const pid = proc.pid + if (!pid || opts?.exited?.()) return - if (process.platform === "win32") { - await new Promise((resolve) => { - const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"], { - stdio: "ignore", - windowsHide: true, - }) - killer.once("exit", () => resolve()) - killer.once("error", () => resolve()) + if (process.platform === "win32") { + await new Promise((resolve) => { + const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"], { + stdio: "ignore", + windowsHide: true, }) - return - } + killer.once("exit", () => resolve()) + killer.once("error", () => resolve()) + }) + return + } - try { - process.kill(-pid, "SIGTERM") - await sleep(SIGKILL_TIMEOUT_MS) - if (!opts?.exited?.()) { - process.kill(-pid, "SIGKILL") - } - } catch (_e) { - proc.kill("SIGTERM") - await sleep(SIGKILL_TIMEOUT_MS) - if (!opts?.exited?.()) { - proc.kill("SIGKILL") - } + try { + process.kill(-pid, "SIGTERM") + await sleep(SIGKILL_TIMEOUT_MS) + if (!opts?.exited?.()) { + process.kill(-pid, "SIGKILL") + } + } catch (_e) { + proc.kill("SIGTERM") + await sleep(SIGKILL_TIMEOUT_MS) + if (!opts?.exited?.()) { + proc.kill("SIGKILL") } } - - function full(file: string) { - if (process.platform !== "win32") return file - const shell = Filesystem.windowsPath(file) - if (path.win32.dirname(shell) !== ".") { - if (shell.startsWith("/") && name(shell) === "bash") return gitbash() || shell - return shell - } - return which(shell) || shell - } - - function pick() { - const pwsh = which("pwsh.exe") - if (pwsh) return pwsh - const powershell = which("powershell.exe") - if (powershell) return powershell - } - - function select(file: string | undefined, opts?: { acceptable?: boolean }) { - if (file && (!opts?.acceptable || !BLACKLIST.has(name(file)))) return full(file) - if (process.platform === "win32") { - const shell = pick() - if (shell) return shell - } - return fallback() - } - - export function gitbash() { - if (process.platform !== "win32") return - if (Flag.OPENCODE_GIT_BASH_PATH) return Flag.OPENCODE_GIT_BASH_PATH - const git = which("git") - if (!git) return - const file = path.join(git, "..", "..", "bin", "bash.exe") - if (Filesystem.stat(file)?.size) return file - } - - function fallback() { - if (process.platform === "win32") { - const file = gitbash() - if (file) return file - return process.env.COMSPEC || "cmd.exe" - } - if (process.platform === "darwin") return "/bin/zsh" - const bash = which("bash") - if (bash) return bash - return "/bin/sh" - } - - export function name(file: string) { - if (process.platform === "win32") return path.win32.parse(Filesystem.windowsPath(file)).name.toLowerCase() - return path.basename(file).toLowerCase() - } - - export function login(file: string) { - return LOGIN.has(name(file)) - } - - export function posix(file: string) { - return POSIX.has(name(file)) - } - - export const preferred = lazy(() => select(process.env.SHELL)) - - export const acceptable = lazy(() => select(process.env.SHELL, { acceptable: true })) } + +function full(file: string) { + if (process.platform !== "win32") return file + const shell = Filesystem.windowsPath(file) + if (path.win32.dirname(shell) !== ".") { + if (shell.startsWith("/") && name(shell) === "bash") return gitbash() || shell + return shell + } + return which(shell) || shell +} + +function pick() { + const pwsh = which("pwsh.exe") + if (pwsh) return pwsh + const powershell = which("powershell.exe") + if (powershell) return powershell +} + +function select(file: string | undefined, opts?: { acceptable?: boolean }) { + if (file && (!opts?.acceptable || !BLACKLIST.has(name(file)))) return full(file) + if (process.platform === "win32") { + const shell = pick() + if (shell) return shell + } + return fallback() +} + +export function gitbash() { + if (process.platform !== "win32") return + if (Flag.OPENCODE_GIT_BASH_PATH) return Flag.OPENCODE_GIT_BASH_PATH + const git = which("git") + if (!git) return + const file = path.join(git, "..", "..", "bin", "bash.exe") + if (Filesystem.stat(file)?.size) return file +} + +function fallback() { + if (process.platform === "win32") { + const file = gitbash() + if (file) return file + return process.env.COMSPEC || "cmd.exe" + } + if (process.platform === "darwin") return "/bin/zsh" + const bash = which("bash") + if (bash) return bash + return "/bin/sh" +} + +export function name(file: string) { + if (process.platform === "win32") return path.win32.parse(Filesystem.windowsPath(file)).name.toLowerCase() + return path.basename(file).toLowerCase() +} + +export function login(file: string) { + return LOGIN.has(name(file)) +} + +export function posix(file: string) { + return POSIX.has(name(file)) +} + +export const preferred = lazy(() => select(process.env.SHELL)) + +export const acceptable = lazy(() => select(process.env.SHELL, { acceptable: true })) + +export * as Shell from "./shell" From 39342b0e759a265045a2b49f34af8ae5da773a8c Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 16 Apr 2026 20:28:08 -0400 Subject: [PATCH 032/335] tui: fix Windows terminal suspend and input undo keybindings On Windows, native terminals don't support POSIX suspend (ctrl+z), so we now assign ctrl+z to input undo instead of terminal suspend. Terminal suspend is disabled on Windows to avoid conflicts with the undo functionality. --- .../src/cli/cmd/tui/config/tui-migrate.ts | 1 - .../opencode/src/cli/cmd/tui/config/tui.ts | 336 +++++++++--------- packages/opencode/src/config/config.ts | 1 - packages/opencode/src/config/keybinds.ts | 14 +- 4 files changed, 183 insertions(+), 169 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts b/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts index 3ce5c4b739..9323dd979a 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts @@ -26,7 +26,6 @@ const TuiLegacy = z interface MigrateInput { cwd: string directories: string[] - custom?: string } /** diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index d264273bca..6e5296db87 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -17,197 +17,203 @@ import { InstallationLocal, InstallationVersion } from "@/installation/version" import { makeRuntime } from "@/cli/effect/runtime" import { Filesystem, Log } from "@/util" -const log = Log.create({ service: "tui.config" }) +export namespace TuiConfig { + const log = Log.create({ service: "tui.config" }) -export const Info = TuiInfo + export const Info = TuiInfo -type Acc = { - result: Info -} + type Acc = { + result: Info + } -type State = { - config: Info - deps: Array> -} + type State = { + config: Info + deps: Array> + } -export type Info = z.output & { - // Internal resolved plugin list used by runtime loading. - plugin_origins?: ConfigPlugin.Origin[] -} + export type Info = z.output & { + // Internal resolved plugin list used by runtime loading. + plugin_origins?: ConfigPlugin.Origin[] + } -export interface Interface { - readonly get: () => Effect.Effect - readonly waitForDependencies: () => Effect.Effect -} + export interface Interface { + readonly get: () => Effect.Effect + readonly waitForDependencies: () => Effect.Effect + } -export class Service extends Context.Service()("@opencode/TuiConfig") {} + export class Service extends Context.Service()("@opencode/TuiConfig") {} -function pluginScope(file: string, ctx: { directory: string }): ConfigPlugin.Scope { - if (Filesystem.contains(ctx.directory, file)) return "local" - // if (ctx.worktree !== "/" && Filesystem.contains(ctx.worktree, file)) return "local" - return "global" -} + function pluginScope(file: string, ctx: { directory: string }): ConfigPlugin.Scope { + if (Filesystem.contains(ctx.directory, file)) return "local" + // if (ctx.worktree !== "/" && Filesystem.contains(ctx.worktree, file)) return "local" + return "global" + } -function customPath() { - return Flag.OPENCODE_TUI_CONFIG -} + function normalize(raw: Record) { + const data = { ...raw } + if (!("tui" in data)) return data + if (!isRecord(data.tui)) { + delete data.tui + return data + } -function normalize(raw: Record) { - const data = { ...raw } - if (!("tui" in data)) return data - if (!isRecord(data.tui)) { + const tui = data.tui delete data.tui - return data - } - - const tui = data.tui - delete data.tui - return { - ...tui, - ...data, - } -} - -async function resolvePlugins(config: Info, configFilepath: string) { - if (!config.plugin) return config - for (let i = 0; i < config.plugin.length; i++) { - config.plugin[i] = await ConfigPlugin.resolvePluginSpec(config.plugin[i], configFilepath) - } - return config -} - -async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) { - const data = await loadFile(file) - acc.result = mergeDeep(acc.result, data) - if (!data.plugin?.length) return - - const scope = pluginScope(file, ctx) - const plugins = ConfigPlugin.deduplicatePluginOrigins([ - ...(acc.result.plugin_origins ?? []), - ...data.plugin.map((spec) => ({ spec, scope, source: file })), - ]) - acc.result.plugin = plugins.map((item) => item.spec) - acc.result.plugin_origins = plugins -} - -async function loadState(ctx: { directory: string }) { - let projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("tui", ctx.directory) - const directories = await ConfigPaths.directories(ctx.directory) - const custom = customPath() - await migrateTuiConfig({ directories, custom, cwd: ctx.directory }) - // Re-compute after migration since migrateTuiConfig may have created new tui.json files - projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("tui", ctx.directory) - - const acc: Acc = { - result: {}, - } - - for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) { - await mergeFile(acc, file, ctx) - } - - if (custom) { - await mergeFile(acc, custom, ctx) - log.debug("loaded custom tui config", { path: custom }) - } - - for (const file of projectFiles) { - await mergeFile(acc, file, ctx) - } - - const dirs = unique(directories).filter((dir) => dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) - - for (const dir of dirs) { - if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue - for (const file of ConfigPaths.fileInDirectory(dir, "tui")) { - await mergeFile(acc, file, ctx) + return { + ...tui, + ...data, } } - const keybinds = { ...(acc.result.keybinds ?? {}) } - if (process.platform === "win32") { - // Native Windows terminals do not support POSIX suspend, so prefer prompt undo. - keybinds.terminal_suspend = "none" - keybinds.input_undo ??= unique([ - "ctrl+z", - ...ConfigKeybinds.Keybinds.shape.input_undo.parse(undefined).split(","), - ]).join(",") + async function resolvePlugins(config: Info, configFilepath: string) { + if (!config.plugin) return config + for (let i = 0; i < config.plugin.length; i++) { + config.plugin[i] = await ConfigPlugin.resolvePluginSpec(config.plugin[i], configFilepath) + } + return config } - acc.result.keybinds = ConfigKeybinds.Keybinds.parse(keybinds) - return { - config: acc.result, - dirs: acc.result.plugin?.length ? dirs : [], + async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) { + const data = await loadFile(file) + acc.result = mergeDeep(acc.result, data) + if (!data.plugin?.length) return + + const scope = pluginScope(file, ctx) + const plugins = ConfigPlugin.deduplicatePluginOrigins([ + ...(acc.result.plugin_origins ?? []), + ...data.plugin.map((spec) => ({ spec, scope, source: file })), + ]) + acc.result.plugin = plugins.map((item) => item.spec) + acc.result.plugin_origins = plugins } -} -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const directory = yield* CurrentWorkingDirectory - const npm = yield* Npm.Service - const data = yield* Effect.promise(() => loadState({ directory })) - const deps = yield* Effect.forEach( - data.dirs, - (dir) => - npm - .install(dir, { - add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)], - }) - .pipe(Effect.forkScoped), - { - concurrency: "unbounded", - }, - ) + async function loadState(ctx: { directory: string }) { + // Every config dir we may read from: global config dir, any `.opencode` + // folders between cwd and home, and OPENCODE_CONFIG_DIR. + const directories = await ConfigPaths.directories(ctx.directory) + // One-time migration: extract tui keys (theme/keybinds/tui) from existing + // opencode.json files into sibling tui.json files. + await migrateTuiConfig({ directories, cwd: ctx.directory }) - const get = Effect.fn("TuiConfig.get")(() => Effect.succeed(data.config)) + const projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG + ? [] + : await ConfigPaths.projectFiles("tui", ctx.directory) - const waitForDependencies = Effect.fn("TuiConfig.waitForDependencies")(() => - Effect.forEach(deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.ignore(), Effect.asVoid), - ) - return Service.of({ get, waitForDependencies }) - }).pipe(Effect.withSpan("TuiConfig.layer")), -) + const acc: Acc = { + result: {}, + } -export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer)) + // 1. Global tui config (lowest precedence). + for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) { + await mergeFile(acc, file, ctx) + } -const { runPromise } = makeRuntime(Service, defaultLayer) + // 2. Explicit OPENCODE_TUI_CONFIG override, if set. + if (Flag.OPENCODE_TUI_CONFIG) { + await mergeFile(acc, Flag.OPENCODE_TUI_CONFIG, ctx) + log.debug("loaded custom tui config", { path: Flag.OPENCODE_TUI_CONFIG }) + } -export async function waitForDependencies() { - await runPromise((svc) => svc.waitForDependencies()) -} + // 3. Project tui files, applied root-first so the closest file wins. + for (const file of projectFiles) { + await mergeFile(acc, file, ctx) + } -export async function get() { - return runPromise((svc) => svc.get()) -} + // 4. `.opencode` directories (and OPENCODE_CONFIG_DIR) discovered while + // walking up the tree. Also returned below so callers can install plugin + // dependencies from each location. + const dirs = unique(directories).filter((dir) => dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) -async function loadFile(filepath: string): Promise { - const text = await ConfigPaths.readFile(filepath) - if (!text) return {} - return load(text, filepath).catch((error) => { - log.warn("failed to load tui config", { path: filepath, error }) - return {} - }) -} + for (const dir of dirs) { + if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue + for (const file of ConfigPaths.fileInDirectory(dir, "tui")) { + await mergeFile(acc, file, ctx) + } + } -async function load(text: string, configFilepath: string): Promise { - return ConfigParse.load(Info, text, { - type: "path", - path: configFilepath, - missing: "empty", - normalize: (data) => { - if (!isRecord(data)) return {} + const keybinds = { ...(acc.result.keybinds ?? {}) } + if (process.platform === "win32") { + // Native Windows terminals do not support POSIX suspend, so prefer prompt undo. + keybinds.terminal_suspend = "none" + keybinds.input_undo ??= unique([ + "ctrl+z", + ...ConfigKeybinds.Keybinds.shape.input_undo.parse(undefined).split(","), + ]).join(",") + } + acc.result.keybinds = ConfigKeybinds.Keybinds.parse(keybinds) - // Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json - // (mirroring the old opencode.json shape) still get their settings applied. - return normalize(data) - }, - }) - .then((data) => resolvePlugins(data, configFilepath)) - .catch((error) => { - log.warn("invalid tui config", { path: configFilepath, error }) + return { + config: acc.result, + dirs: acc.result.plugin?.length ? dirs : [], + } + } + + export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const directory = yield* CurrentWorkingDirectory + const npm = yield* Npm.Service + const data = yield* Effect.promise(() => loadState({ directory })) + const deps = yield* Effect.forEach( + data.dirs, + (dir) => + npm + .install(dir, { + add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)], + }) + .pipe(Effect.forkScoped), + { + concurrency: "unbounded", + }, + ) + + const get = Effect.fn("TuiConfig.get")(() => Effect.succeed(data.config)) + + const waitForDependencies = Effect.fn("TuiConfig.waitForDependencies")(() => + Effect.forEach(deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.ignore(), Effect.asVoid), + ) + return Service.of({ get, waitForDependencies }) + }).pipe(Effect.withSpan("TuiConfig.layer")), + ) + + export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer)) + + const { runPromise } = makeRuntime(Service, defaultLayer) + + export async function waitForDependencies() { + await runPromise((svc) => svc.waitForDependencies()) + } + + export async function get() { + return runPromise((svc) => svc.get()) + } + + async function loadFile(filepath: string): Promise { + const text = await ConfigPaths.readFile(filepath) + if (!text) return {} + return load(text, filepath).catch((error) => { + log.warn("failed to load tui config", { path: filepath, error }) return {} }) -} + } -export * as TuiConfig from "./tui" + async function load(text: string, configFilepath: string): Promise { + return ConfigParse.load(Info, text, { + type: "path", + path: configFilepath, + missing: "empty", + normalize: (data) => { + if (!isRecord(data)) return {} + + // Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json + // (mirroring the old opencode.json shape) still get their settings applied. + return normalize(data) + }, + }) + .then((data) => resolvePlugins(data, configFilepath)) + .catch((error) => { + log.warn("invalid tui config", { path: configFilepath, error }) + return {} + }) + } +} diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 3cbc539600..adccb6353b 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -19,7 +19,6 @@ import { GlobalBus } from "@/bus/global" import { Event } from "../server/event" import { Account } from "@/account" import { isRecord } from "@/util/record" -import { InvalidError, JsonError } from "./error" import type { ConsoleState } from "./console-state" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { InstanceState } from "@/effect" diff --git a/packages/opencode/src/config/keybinds.ts b/packages/opencode/src/config/keybinds.ts index cb146b7cae..8a22289d2a 100644 --- a/packages/opencode/src/config/keybinds.ts +++ b/packages/opencode/src/config/keybinds.ts @@ -106,7 +106,12 @@ export const Keybinds = z input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"), input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"), input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"), - input_undo: z.string().optional().default("ctrl+-,super+z").describe("Undo in input"), + input_undo: z + .string() + .optional() + // On Windows prepend ctrl+z since terminal_suspend releases the binding. + .default(process.platform === "win32" ? "ctrl+z,ctrl+-,super+z" : "ctrl+-,super+z") + .describe("Undo in input"), input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"), input_word_forward: z .string() @@ -144,7 +149,12 @@ export const Keybinds = z session_child_cycle: z.string().optional().default("right").describe("Go to next child session"), session_child_cycle_reverse: z.string().optional().default("left").describe("Go to previous child session"), session_parent: z.string().optional().default("up").describe("Go to parent session"), - terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"), + terminal_suspend: z + .string() + .optional() + .default("ctrl+z") + .transform((v) => (process.platform === "win32" ? "none" : v)) + .describe("Suspend terminal"), terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"), tips_toggle: z.string().optional().default("h").describe("Toggle tips on home screen"), plugin_manager: z.string().optional().default("none").describe("Open plugin manager dialog"), From d6af5a686cd45dc68504283645de990bbeaf2005 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 16 Apr 2026 20:46:40 -0400 Subject: [PATCH 033/335] tui: convert TuiConfig namespace to ES module exports --- .../opencode/src/cli/cmd/tui/config/tui.ts | 372 +++++++++--------- 1 file changed, 185 insertions(+), 187 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index 6e5296db87..b55cf3b83f 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -1,3 +1,5 @@ +export * as TuiConfig from "./tui" + import z from "zod" import { mergeDeep, unique } from "remeda" import { Context, Effect, Fiber, Layer } from "effect" @@ -17,203 +19,199 @@ import { InstallationLocal, InstallationVersion } from "@/installation/version" import { makeRuntime } from "@/cli/effect/runtime" import { Filesystem, Log } from "@/util" -export namespace TuiConfig { - const log = Log.create({ service: "tui.config" }) +const log = Log.create({ service: "tui.config" }) - export const Info = TuiInfo +export const Info = TuiInfo - type Acc = { - result: Info - } +type Acc = { + result: Info +} - type State = { - config: Info - deps: Array> - } +type State = { + config: Info + deps: Array> +} - export type Info = z.output & { - // Internal resolved plugin list used by runtime loading. - plugin_origins?: ConfigPlugin.Origin[] - } +export type Info = z.output & { + // Internal resolved plugin list used by runtime loading. + plugin_origins?: ConfigPlugin.Origin[] +} - export interface Interface { - readonly get: () => Effect.Effect - readonly waitForDependencies: () => Effect.Effect - } +export interface Interface { + readonly get: () => Effect.Effect + readonly waitForDependencies: () => Effect.Effect +} - export class Service extends Context.Service()("@opencode/TuiConfig") {} +export class Service extends Context.Service()("@opencode/TuiConfig") {} - function pluginScope(file: string, ctx: { directory: string }): ConfigPlugin.Scope { - if (Filesystem.contains(ctx.directory, file)) return "local" - // if (ctx.worktree !== "/" && Filesystem.contains(ctx.worktree, file)) return "local" - return "global" - } +function pluginScope(file: string, ctx: { directory: string }): ConfigPlugin.Scope { + if (Filesystem.contains(ctx.directory, file)) return "local" + // if (ctx.worktree !== "/" && Filesystem.contains(ctx.worktree, file)) return "local" + return "global" +} - function normalize(raw: Record) { - const data = { ...raw } - if (!("tui" in data)) return data - if (!isRecord(data.tui)) { - delete data.tui - return data - } - - const tui = data.tui +function normalize(raw: Record) { + const data = { ...raw } + if (!("tui" in data)) return data + if (!isRecord(data.tui)) { delete data.tui - return { - ...tui, - ...data, - } + return data } - async function resolvePlugins(config: Info, configFilepath: string) { - if (!config.plugin) return config - for (let i = 0; i < config.plugin.length; i++) { - config.plugin[i] = await ConfigPlugin.resolvePluginSpec(config.plugin[i], configFilepath) - } - return config - } - - async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) { - const data = await loadFile(file) - acc.result = mergeDeep(acc.result, data) - if (!data.plugin?.length) return - - const scope = pluginScope(file, ctx) - const plugins = ConfigPlugin.deduplicatePluginOrigins([ - ...(acc.result.plugin_origins ?? []), - ...data.plugin.map((spec) => ({ spec, scope, source: file })), - ]) - acc.result.plugin = plugins.map((item) => item.spec) - acc.result.plugin_origins = plugins - } - - async function loadState(ctx: { directory: string }) { - // Every config dir we may read from: global config dir, any `.opencode` - // folders between cwd and home, and OPENCODE_CONFIG_DIR. - const directories = await ConfigPaths.directories(ctx.directory) - // One-time migration: extract tui keys (theme/keybinds/tui) from existing - // opencode.json files into sibling tui.json files. - await migrateTuiConfig({ directories, cwd: ctx.directory }) - - const projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG - ? [] - : await ConfigPaths.projectFiles("tui", ctx.directory) - - const acc: Acc = { - result: {}, - } - - // 1. Global tui config (lowest precedence). - for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) { - await mergeFile(acc, file, ctx) - } - - // 2. Explicit OPENCODE_TUI_CONFIG override, if set. - if (Flag.OPENCODE_TUI_CONFIG) { - await mergeFile(acc, Flag.OPENCODE_TUI_CONFIG, ctx) - log.debug("loaded custom tui config", { path: Flag.OPENCODE_TUI_CONFIG }) - } - - // 3. Project tui files, applied root-first so the closest file wins. - for (const file of projectFiles) { - await mergeFile(acc, file, ctx) - } - - // 4. `.opencode` directories (and OPENCODE_CONFIG_DIR) discovered while - // walking up the tree. Also returned below so callers can install plugin - // dependencies from each location. - const dirs = unique(directories).filter((dir) => dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) - - for (const dir of dirs) { - if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue - for (const file of ConfigPaths.fileInDirectory(dir, "tui")) { - await mergeFile(acc, file, ctx) - } - } - - const keybinds = { ...(acc.result.keybinds ?? {}) } - if (process.platform === "win32") { - // Native Windows terminals do not support POSIX suspend, so prefer prompt undo. - keybinds.terminal_suspend = "none" - keybinds.input_undo ??= unique([ - "ctrl+z", - ...ConfigKeybinds.Keybinds.shape.input_undo.parse(undefined).split(","), - ]).join(",") - } - acc.result.keybinds = ConfigKeybinds.Keybinds.parse(keybinds) - - return { - config: acc.result, - dirs: acc.result.plugin?.length ? dirs : [], - } - } - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const directory = yield* CurrentWorkingDirectory - const npm = yield* Npm.Service - const data = yield* Effect.promise(() => loadState({ directory })) - const deps = yield* Effect.forEach( - data.dirs, - (dir) => - npm - .install(dir, { - add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)], - }) - .pipe(Effect.forkScoped), - { - concurrency: "unbounded", - }, - ) - - const get = Effect.fn("TuiConfig.get")(() => Effect.succeed(data.config)) - - const waitForDependencies = Effect.fn("TuiConfig.waitForDependencies")(() => - Effect.forEach(deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.ignore(), Effect.asVoid), - ) - return Service.of({ get, waitForDependencies }) - }).pipe(Effect.withSpan("TuiConfig.layer")), - ) - - export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer)) - - const { runPromise } = makeRuntime(Service, defaultLayer) - - export async function waitForDependencies() { - await runPromise((svc) => svc.waitForDependencies()) - } - - export async function get() { - return runPromise((svc) => svc.get()) - } - - async function loadFile(filepath: string): Promise { - const text = await ConfigPaths.readFile(filepath) - if (!text) return {} - return load(text, filepath).catch((error) => { - log.warn("failed to load tui config", { path: filepath, error }) - return {} - }) - } - - async function load(text: string, configFilepath: string): Promise { - return ConfigParse.load(Info, text, { - type: "path", - path: configFilepath, - missing: "empty", - normalize: (data) => { - if (!isRecord(data)) return {} - - // Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json - // (mirroring the old opencode.json shape) still get their settings applied. - return normalize(data) - }, - }) - .then((data) => resolvePlugins(data, configFilepath)) - .catch((error) => { - log.warn("invalid tui config", { path: configFilepath, error }) - return {} - }) + const tui = data.tui + delete data.tui + return { + ...tui, + ...data, } } + +async function resolvePlugins(config: Info, configFilepath: string) { + if (!config.plugin) return config + for (let i = 0; i < config.plugin.length; i++) { + config.plugin[i] = await ConfigPlugin.resolvePluginSpec(config.plugin[i], configFilepath) + } + return config +} + +async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) { + const data = await loadFile(file) + acc.result = mergeDeep(acc.result, data) + if (!data.plugin?.length) return + + const scope = pluginScope(file, ctx) + const plugins = ConfigPlugin.deduplicatePluginOrigins([ + ...(acc.result.plugin_origins ?? []), + ...data.plugin.map((spec) => ({ spec, scope, source: file })), + ]) + acc.result.plugin = plugins.map((item) => item.spec) + acc.result.plugin_origins = plugins +} + +async function loadState(ctx: { directory: string }) { + // Every config dir we may read from: global config dir, any `.opencode` + // folders between cwd and home, and OPENCODE_CONFIG_DIR. + const directories = await ConfigPaths.directories(ctx.directory) + // One-time migration: extract tui keys (theme/keybinds/tui) from existing + // opencode.json files into sibling tui.json files. + await migrateTuiConfig({ directories, cwd: ctx.directory }) + + const projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("tui", ctx.directory) + + const acc: Acc = { + result: {}, + } + + // 1. Global tui config (lowest precedence). + for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) { + await mergeFile(acc, file, ctx) + } + + // 2. Explicit OPENCODE_TUI_CONFIG override, if set. + if (Flag.OPENCODE_TUI_CONFIG) { + await mergeFile(acc, Flag.OPENCODE_TUI_CONFIG, ctx) + log.debug("loaded custom tui config", { path: Flag.OPENCODE_TUI_CONFIG }) + } + + // 3. Project tui files, applied root-first so the closest file wins. + for (const file of projectFiles) { + await mergeFile(acc, file, ctx) + } + + // 4. `.opencode` directories (and OPENCODE_CONFIG_DIR) discovered while + // walking up the tree. Also returned below so callers can install plugin + // dependencies from each location. + const dirs = unique(directories).filter((dir) => dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) + + for (const dir of dirs) { + if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue + for (const file of ConfigPaths.fileInDirectory(dir, "tui")) { + await mergeFile(acc, file, ctx) + } + } + + const keybinds = { ...(acc.result.keybinds ?? {}) } + if (process.platform === "win32") { + // Native Windows terminals do not support POSIX suspend, so prefer prompt undo. + keybinds.terminal_suspend = "none" + keybinds.input_undo ??= unique([ + "ctrl+z", + ...ConfigKeybinds.Keybinds.shape.input_undo.parse(undefined).split(","), + ]).join(",") + } + acc.result.keybinds = ConfigKeybinds.Keybinds.parse(keybinds) + + return { + config: acc.result, + dirs: acc.result.plugin?.length ? dirs : [], + } +} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const directory = yield* CurrentWorkingDirectory + const npm = yield* Npm.Service + const data = yield* Effect.promise(() => loadState({ directory })) + const deps = yield* Effect.forEach( + data.dirs, + (dir) => + npm + .install(dir, { + add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)], + }) + .pipe(Effect.forkScoped), + { + concurrency: "unbounded", + }, + ) + + const get = Effect.fn("TuiConfig.get")(() => Effect.succeed(data.config)) + + const waitForDependencies = Effect.fn("TuiConfig.waitForDependencies")(() => + Effect.forEach(deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.ignore(), Effect.asVoid), + ) + return Service.of({ get, waitForDependencies }) + }).pipe(Effect.withSpan("TuiConfig.layer")), +) + +export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer)) + +const { runPromise } = makeRuntime(Service, defaultLayer) + +export async function waitForDependencies() { + await runPromise((svc) => svc.waitForDependencies()) +} + +export async function get() { + return runPromise((svc) => svc.get()) +} + +async function loadFile(filepath: string): Promise { + const text = await ConfigPaths.readFile(filepath) + if (!text) return {} + return load(text, filepath).catch((error) => { + log.warn("failed to load tui config", { path: filepath, error }) + return {} + }) +} + +async function load(text: string, configFilepath: string): Promise { + return ConfigParse.load(Info, text, { + type: "path", + path: configFilepath, + missing: "empty", + normalize: (data) => { + if (!isRecord(data)) return {} + + // Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json + // (mirroring the old opencode.json shape) still get their settings applied. + return normalize(data) + }, + }) + .then((data) => resolvePlugins(data, configFilepath)) + .catch((error) => { + log.warn("invalid tui config", { path: configFilepath, error }) + return {} + }) +} From 51d8219c46f902b90cee716f3b8475e163e21e7c Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:49:39 -0400 Subject: [PATCH 034/335] refactor: unwrap session/ tier-2 namespaces + self-reexport (#22973) --- packages/opencode/src/session/compaction.ts | 666 ++-- packages/opencode/src/session/instruction.ts | 318 +- packages/opencode/src/session/llm.ts | 824 ++--- packages/opencode/src/session/message-v2.ts | 1908 +++++------ packages/opencode/src/session/message.ts | 346 +- packages/opencode/src/session/processor.ts | 1158 +++---- packages/opencode/src/session/prompt.ts | 3174 +++++++++--------- packages/opencode/src/session/retry.ts | 232 +- packages/opencode/src/session/revert.ts | 290 +- packages/opencode/src/session/run-state.ts | 198 +- packages/opencode/src/session/status.ts | 156 +- packages/opencode/src/session/summary.ts | 274 +- packages/opencode/src/session/system.ts | 126 +- packages/opencode/src/session/todo.ts | 148 +- 14 files changed, 4909 insertions(+), 4909 deletions(-) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 3ef6977547..212f5fdbab 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -17,173 +17,172 @@ import { Effect, Layer, Context } from "effect" import { InstanceState } from "@/effect" import { isOverflow as overflow } from "./overflow" -export namespace SessionCompaction { - const log = Log.create({ service: "session.compaction" }) +const log = Log.create({ service: "session.compaction" }) - export const Event = { - Compacted: BusEvent.define( - "session.compacted", - z.object({ - sessionID: SessionID.zod, - }), - ), - } +export const Event = { + Compacted: BusEvent.define( + "session.compacted", + z.object({ + sessionID: SessionID.zod, + }), + ), +} - export const PRUNE_MINIMUM = 20_000 - export const PRUNE_PROTECT = 40_000 - const PRUNE_PROTECTED_TOOLS = ["skill"] +export const PRUNE_MINIMUM = 20_000 +export const PRUNE_PROTECT = 40_000 +const PRUNE_PROTECTED_TOOLS = ["skill"] - export interface Interface { - readonly isOverflow: (input: { +export interface Interface { + readonly isOverflow: (input: { + tokens: MessageV2.Assistant["tokens"] + model: Provider.Model + }) => Effect.Effect + readonly prune: (input: { sessionID: SessionID }) => Effect.Effect + readonly process: (input: { + parentID: MessageID + messages: MessageV2.WithParts[] + sessionID: SessionID + auto: boolean + overflow?: boolean + }) => Effect.Effect<"continue" | "stop"> + readonly create: (input: { + sessionID: SessionID + agent: string + model: { providerID: ProviderID; modelID: ModelID } + auto: boolean + overflow?: boolean + }) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/SessionCompaction") {} + +export const layer: Layer.Layer< + Service, + never, + | Bus.Service + | Config.Service + | Session.Service + | Agent.Service + | Plugin.Service + | SessionProcessor.Service + | Provider.Service +> = Layer.effect( + Service, + Effect.gen(function* () { + const bus = yield* Bus.Service + const config = yield* Config.Service + const session = yield* Session.Service + const agents = yield* Agent.Service + const plugin = yield* Plugin.Service + const processors = yield* SessionProcessor.Service + const provider = yield* Provider.Service + + const isOverflow = Effect.fn("SessionCompaction.isOverflow")(function* (input: { tokens: MessageV2.Assistant["tokens"] model: Provider.Model - }) => Effect.Effect - readonly prune: (input: { sessionID: SessionID }) => Effect.Effect - readonly process: (input: { + }) { + return overflow({ cfg: yield* config.get(), tokens: input.tokens, model: input.model }) + }) + + // goes backwards through parts until there are PRUNE_PROTECT tokens worth of tool + // calls, then erases output of older tool calls to free context space + const prune = Effect.fn("SessionCompaction.prune")(function* (input: { sessionID: SessionID }) { + const cfg = yield* config.get() + if (cfg.compaction?.prune === false) return + log.info("pruning") + + const msgs = yield* session + .messages({ sessionID: input.sessionID }) + .pipe(Effect.catchIf(NotFoundError.isInstance, () => Effect.succeed(undefined))) + if (!msgs) return + + let total = 0 + let pruned = 0 + const toPrune: MessageV2.ToolPart[] = [] + let turns = 0 + + loop: for (let msgIndex = msgs.length - 1; msgIndex >= 0; msgIndex--) { + const msg = msgs[msgIndex] + if (msg.info.role === "user") turns++ + if (turns < 2) continue + if (msg.info.role === "assistant" && msg.info.summary) break loop + for (let partIndex = msg.parts.length - 1; partIndex >= 0; partIndex--) { + const part = msg.parts[partIndex] + if (part.type === "tool") + if (part.state.status === "completed") { + if (PRUNE_PROTECTED_TOOLS.includes(part.tool)) continue + if (part.state.time.compacted) break loop + const estimate = Token.estimate(part.state.output) + total += estimate + if (total > PRUNE_PROTECT) { + pruned += estimate + toPrune.push(part) + } + } + } + } + + log.info("found", { pruned, total }) + if (pruned > PRUNE_MINIMUM) { + for (const part of toPrune) { + if (part.state.status === "completed") { + part.state.time.compacted = Date.now() + yield* session.updatePart(part) + } + } + log.info("pruned", { count: toPrune.length }) + } + }) + + const processCompaction = Effect.fn("SessionCompaction.process")(function* (input: { parentID: MessageID messages: MessageV2.WithParts[] sessionID: SessionID auto: boolean overflow?: boolean - }) => Effect.Effect<"continue" | "stop"> - readonly create: (input: { - sessionID: SessionID - agent: string - model: { providerID: ProviderID; modelID: ModelID } - auto: boolean - overflow?: boolean - }) => Effect.Effect - } + }) { + const parent = input.messages.findLast((m) => m.info.id === input.parentID) + if (!parent || parent.info.role !== "user") { + throw new Error(`Compaction parent must be a user message: ${input.parentID}`) + } + const userMessage = parent.info - export class Service extends Context.Service()("@opencode/SessionCompaction") {} - - export const layer: Layer.Layer< - Service, - never, - | Bus.Service - | Config.Service - | Session.Service - | Agent.Service - | Plugin.Service - | SessionProcessor.Service - | Provider.Service - > = Layer.effect( - Service, - Effect.gen(function* () { - const bus = yield* Bus.Service - const config = yield* Config.Service - const session = yield* Session.Service - const agents = yield* Agent.Service - const plugin = yield* Plugin.Service - const processors = yield* SessionProcessor.Service - const provider = yield* Provider.Service - - const isOverflow = Effect.fn("SessionCompaction.isOverflow")(function* (input: { - tokens: MessageV2.Assistant["tokens"] - model: Provider.Model - }) { - return overflow({ cfg: yield* config.get(), tokens: input.tokens, model: input.model }) - }) - - // goes backwards through parts until there are PRUNE_PROTECT tokens worth of tool - // calls, then erases output of older tool calls to free context space - const prune = Effect.fn("SessionCompaction.prune")(function* (input: { sessionID: SessionID }) { - const cfg = yield* config.get() - if (cfg.compaction?.prune === false) return - log.info("pruning") - - const msgs = yield* session - .messages({ sessionID: input.sessionID }) - .pipe(Effect.catchIf(NotFoundError.isInstance, () => Effect.succeed(undefined))) - if (!msgs) return - - let total = 0 - let pruned = 0 - const toPrune: MessageV2.ToolPart[] = [] - let turns = 0 - - loop: for (let msgIndex = msgs.length - 1; msgIndex >= 0; msgIndex--) { - const msg = msgs[msgIndex] - if (msg.info.role === "user") turns++ - if (turns < 2) continue - if (msg.info.role === "assistant" && msg.info.summary) break loop - for (let partIndex = msg.parts.length - 1; partIndex >= 0; partIndex--) { - const part = msg.parts[partIndex] - if (part.type === "tool") - if (part.state.status === "completed") { - if (PRUNE_PROTECTED_TOOLS.includes(part.tool)) continue - if (part.state.time.compacted) break loop - const estimate = Token.estimate(part.state.output) - total += estimate - if (total > PRUNE_PROTECT) { - pruned += estimate - toPrune.push(part) - } - } + let messages = input.messages + let replay: + | { + info: MessageV2.User + parts: MessageV2.Part[] + } + | undefined + if (input.overflow) { + const idx = input.messages.findIndex((m) => m.info.id === input.parentID) + for (let i = idx - 1; i >= 0; i--) { + const msg = input.messages[i] + if (msg.info.role === "user" && !msg.parts.some((p) => p.type === "compaction")) { + replay = { info: msg.info, parts: msg.parts } + messages = input.messages.slice(0, i) + break } } - - log.info("found", { pruned, total }) - if (pruned > PRUNE_MINIMUM) { - for (const part of toPrune) { - if (part.state.status === "completed") { - part.state.time.compacted = Date.now() - yield* session.updatePart(part) - } - } - log.info("pruned", { count: toPrune.length }) + const hasContent = + replay && messages.some((m) => m.info.role === "user" && !m.parts.some((p) => p.type === "compaction")) + if (!hasContent) { + replay = undefined + messages = input.messages } - }) + } - const processCompaction = Effect.fn("SessionCompaction.process")(function* (input: { - parentID: MessageID - messages: MessageV2.WithParts[] - sessionID: SessionID - auto: boolean - overflow?: boolean - }) { - const parent = input.messages.findLast((m) => m.info.id === input.parentID) - if (!parent || parent.info.role !== "user") { - throw new Error(`Compaction parent must be a user message: ${input.parentID}`) - } - const userMessage = parent.info - - let messages = input.messages - let replay: - | { - info: MessageV2.User - parts: MessageV2.Part[] - } - | undefined - if (input.overflow) { - const idx = input.messages.findIndex((m) => m.info.id === input.parentID) - for (let i = idx - 1; i >= 0; i--) { - const msg = input.messages[i] - if (msg.info.role === "user" && !msg.parts.some((p) => p.type === "compaction")) { - replay = { info: msg.info, parts: msg.parts } - messages = input.messages.slice(0, i) - break - } - } - const hasContent = - replay && messages.some((m) => m.info.role === "user" && !m.parts.some((p) => p.type === "compaction")) - if (!hasContent) { - replay = undefined - messages = input.messages - } - } - - const agent = yield* agents.get("compaction") - const model = agent.model - ? yield* provider.getModel(agent.model.providerID, agent.model.modelID) - : yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID) - // Allow plugins to inject context or replace compaction prompt. - const compacting = yield* plugin.trigger( - "experimental.session.compacting", - { sessionID: input.sessionID }, - { context: [], prompt: undefined }, - ) - const defaultPrompt = `Provide a detailed prompt for continuing our conversation above. + const agent = yield* agents.get("compaction") + const model = agent.model + ? yield* provider.getModel(agent.model.providerID, agent.model.modelID) + : yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID) + // Allow plugins to inject context or replace compaction prompt. + const compacting = yield* plugin.trigger( + "experimental.session.compacting", + { sessionID: input.sessionID }, + { context: [], prompt: undefined }, + ) + const defaultPrompt = `Provide a detailed prompt for continuing our conversation above. Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next. The summary that you construct will be used so that another agent can read it and continue the work. Do not call any tools. Respond only with the summary text. @@ -213,200 +212,201 @@ When constructing the summary, try to stick to this template: [Construct a structured list of relevant files that have been read, edited, or created that pertain to the task at hand. If all the files in a directory are relevant, include the path to the directory.] ---` - const prompt = compacting.prompt ?? [defaultPrompt, ...compacting.context].join("\n\n") - const msgs = structuredClone(messages) - yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs }) - const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, { stripMedia: true }) - const ctx = yield* InstanceState.context - const msg: MessageV2.Assistant = { - id: MessageID.ascending(), - role: "assistant", - parentID: input.parentID, - sessionID: input.sessionID, - mode: "compaction", - agent: "compaction", - variant: userMessage.model.variant, - summary: true, - path: { - cwd: ctx.directory, - root: ctx.worktree, + const prompt = compacting.prompt ?? [defaultPrompt, ...compacting.context].join("\n\n") + const msgs = structuredClone(messages) + yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs }) + const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, { stripMedia: true }) + const ctx = yield* InstanceState.context + const msg: MessageV2.Assistant = { + id: MessageID.ascending(), + role: "assistant", + parentID: input.parentID, + sessionID: input.sessionID, + mode: "compaction", + agent: "compaction", + variant: userMessage.model.variant, + summary: true, + path: { + cwd: ctx.directory, + root: ctx.worktree, + }, + cost: 0, + tokens: { + output: 0, + input: 0, + reasoning: 0, + cache: { read: 0, write: 0 }, + }, + modelID: model.id, + providerID: model.providerID, + time: { + created: Date.now(), + }, + } + yield* session.updateMessage(msg) + const processor = yield* processors.create({ + assistantMessage: msg, + sessionID: input.sessionID, + model, + }) + const result = yield* processor.process({ + user: userMessage, + agent, + sessionID: input.sessionID, + tools: {}, + system: [], + messages: [ + ...modelMessages, + { + role: "user", + content: [{ type: "text", text: prompt }], }, - cost: 0, - tokens: { - output: 0, - input: 0, - reasoning: 0, - cache: { read: 0, write: 0 }, - }, - modelID: model.id, - providerID: model.providerID, - time: { - created: Date.now(), - }, - } - yield* session.updateMessage(msg) - const processor = yield* processors.create({ - assistantMessage: msg, - sessionID: input.sessionID, - model, - }) - const result = yield* processor.process({ - user: userMessage, - agent, - sessionID: input.sessionID, - tools: {}, - system: [], - messages: [ - ...modelMessages, - { - role: "user", - content: [{ type: "text", text: prompt }], - }, - ], - model, - }) + ], + model, + }) - if (result === "compact") { - processor.message.error = new MessageV2.ContextOverflowError({ - message: replay - ? "Conversation history too large to compact - exceeds model context limit" - : "Session too large to compact - context exceeds model limit even after stripping media", - }).toObject() - processor.message.finish = "error" - yield* session.updateMessage(processor.message) - return "stop" + if (result === "compact") { + processor.message.error = new MessageV2.ContextOverflowError({ + message: replay + ? "Conversation history too large to compact - exceeds model context limit" + : "Session too large to compact - context exceeds model limit even after stripping media", + }).toObject() + processor.message.finish = "error" + yield* session.updateMessage(processor.message) + return "stop" + } + + if (result === "continue" && input.auto) { + if (replay) { + const original = replay.info + const replayMsg = yield* session.updateMessage({ + id: MessageID.ascending(), + role: "user", + sessionID: input.sessionID, + time: { created: Date.now() }, + agent: original.agent, + model: original.model, + format: original.format, + tools: original.tools, + system: original.system, + }) + for (const part of replay.parts) { + if (part.type === "compaction") continue + const replayPart = + part.type === "file" && MessageV2.isMedia(part.mime) + ? { type: "text" as const, text: `[Attached ${part.mime}: ${part.filename ?? "file"}]` } + : part + yield* session.updatePart({ + ...replayPart, + id: PartID.ascending(), + messageID: replayMsg.id, + sessionID: input.sessionID, + }) + } } - if (result === "continue" && input.auto) { - if (replay) { - const original = replay.info - const replayMsg = yield* session.updateMessage({ + if (!replay) { + const info = yield* provider.getProvider(userMessage.model.providerID) + if ( + (yield* plugin.trigger( + "experimental.compaction.autocontinue", + { + sessionID: input.sessionID, + agent: userMessage.agent, + model: yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID), + provider: { + source: info.source, + info, + options: info.options, + }, + message: userMessage, + overflow: input.overflow === true, + }, + { enabled: true }, + )).enabled + ) { + const continueMsg = yield* session.updateMessage({ id: MessageID.ascending(), role: "user", sessionID: input.sessionID, time: { created: Date.now() }, - agent: original.agent, - model: original.model, - format: original.format, - tools: original.tools, - system: original.system, + agent: userMessage.agent, + model: userMessage.model, + }) + const text = + (input.overflow + ? "The previous request exceeded the provider's size limit due to large media attachments. The conversation was compacted and media files were removed from context. If the user was asking about attached images or files, explain that the attachments were too large to process and suggest they try again with smaller or fewer files.\n\n" + : "") + + "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed." + yield* session.updatePart({ + id: PartID.ascending(), + messageID: continueMsg.id, + sessionID: input.sessionID, + type: "text", + // Internal marker for auto-compaction followups so provider plugins + // can distinguish them from manual post-compaction user prompts. + // This is not a stable plugin contract and may change or disappear. + metadata: { compaction_continue: true }, + synthetic: true, + text, + time: { + start: Date.now(), + end: Date.now(), + }, }) - for (const part of replay.parts) { - if (part.type === "compaction") continue - const replayPart = - part.type === "file" && MessageV2.isMedia(part.mime) - ? { type: "text" as const, text: `[Attached ${part.mime}: ${part.filename ?? "file"}]` } - : part - yield* session.updatePart({ - ...replayPart, - id: PartID.ascending(), - messageID: replayMsg.id, - sessionID: input.sessionID, - }) - } - } - - if (!replay) { - const info = yield* provider.getProvider(userMessage.model.providerID) - if ( - (yield* plugin.trigger( - "experimental.compaction.autocontinue", - { - sessionID: input.sessionID, - agent: userMessage.agent, - model: yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID), - provider: { - source: info.source, - info, - options: info.options, - }, - message: userMessage, - overflow: input.overflow === true, - }, - { enabled: true }, - )).enabled - ) { - const continueMsg = yield* session.updateMessage({ - id: MessageID.ascending(), - role: "user", - sessionID: input.sessionID, - time: { created: Date.now() }, - agent: userMessage.agent, - model: userMessage.model, - }) - const text = - (input.overflow - ? "The previous request exceeded the provider's size limit due to large media attachments. The conversation was compacted and media files were removed from context. If the user was asking about attached images or files, explain that the attachments were too large to process and suggest they try again with smaller or fewer files.\n\n" - : "") + - "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed." - yield* session.updatePart({ - id: PartID.ascending(), - messageID: continueMsg.id, - sessionID: input.sessionID, - type: "text", - // Internal marker for auto-compaction followups so provider plugins - // can distinguish them from manual post-compaction user prompts. - // This is not a stable plugin contract and may change or disappear. - metadata: { compaction_continue: true }, - synthetic: true, - text, - time: { - start: Date.now(), - end: Date.now(), - }, - }) - } } } + } - if (processor.message.error) return "stop" - if (result === "continue") yield* bus.publish(Event.Compacted, { sessionID: input.sessionID }) - return result + if (processor.message.error) return "stop" + if (result === "continue") yield* bus.publish(Event.Compacted, { sessionID: input.sessionID }) + return result + }) + + const create = Effect.fn("SessionCompaction.create")(function* (input: { + sessionID: SessionID + agent: string + model: { providerID: ProviderID; modelID: ModelID } + auto: boolean + overflow?: boolean + }) { + const msg = yield* session.updateMessage({ + id: MessageID.ascending(), + role: "user", + model: input.model, + sessionID: input.sessionID, + agent: input.agent, + time: { created: Date.now() }, }) - - const create = Effect.fn("SessionCompaction.create")(function* (input: { - sessionID: SessionID - agent: string - model: { providerID: ProviderID; modelID: ModelID } - auto: boolean - overflow?: boolean - }) { - const msg = yield* session.updateMessage({ - id: MessageID.ascending(), - role: "user", - model: input.model, - sessionID: input.sessionID, - agent: input.agent, - time: { created: Date.now() }, - }) - yield* session.updatePart({ - id: PartID.ascending(), - messageID: msg.id, - sessionID: msg.sessionID, - type: "compaction", - auto: input.auto, - overflow: input.overflow, - }) + yield* session.updatePart({ + id: PartID.ascending(), + messageID: msg.id, + sessionID: msg.sessionID, + type: "compaction", + auto: input.auto, + overflow: input.overflow, }) + }) - return Service.of({ - isOverflow, - prune, - process: processCompaction, - create, - }) - }), - ) + return Service.of({ + isOverflow, + prune, + process: processCompaction, + create, + }) + }), +) - export const defaultLayer = Layer.suspend(() => - layer.pipe( - Layer.provide(Provider.defaultLayer), - Layer.provide(Session.defaultLayer), - Layer.provide(SessionProcessor.defaultLayer), - Layer.provide(Agent.defaultLayer), - Layer.provide(Plugin.defaultLayer), - Layer.provide(Bus.layer), - Layer.provide(Config.defaultLayer), - ), - ) -} +export const defaultLayer = Layer.suspend(() => + layer.pipe( + Layer.provide(Provider.defaultLayer), + Layer.provide(Session.defaultLayer), + Layer.provide(SessionProcessor.defaultLayer), + Layer.provide(Agent.defaultLayer), + Layer.provide(Plugin.defaultLayer), + Layer.provide(Bus.layer), + Layer.provide(Config.defaultLayer), + ), +) + +export * as SessionCompaction from "./compaction" diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index cd2050adf5..768f352d93 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -50,194 +50,194 @@ function extract(messages: MessageV2.WithParts[]) { return paths } -export namespace Instruction { - export interface Interface { - readonly clear: (messageID: MessageID) => Effect.Effect - readonly systemPaths: () => Effect.Effect, AppFileSystem.Error> - readonly system: () => Effect.Effect - readonly find: (dir: string) => Effect.Effect - readonly resolve: ( - messages: MessageV2.WithParts[], - filepath: string, - messageID: MessageID, - ) => Effect.Effect<{ filepath: string; content: string }[], AppFileSystem.Error> - } +export interface Interface { + readonly clear: (messageID: MessageID) => Effect.Effect + readonly systemPaths: () => Effect.Effect, AppFileSystem.Error> + readonly system: () => Effect.Effect + readonly find: (dir: string) => Effect.Effect + readonly resolve: ( + messages: MessageV2.WithParts[], + filepath: string, + messageID: MessageID, + ) => Effect.Effect<{ filepath: string; content: string }[], AppFileSystem.Error> +} - export class Service extends Context.Service()("@opencode/Instruction") {} +export class Service extends Context.Service()("@opencode/Instruction") {} - export const layer: Layer.Layer = - Layer.effect( - Service, - Effect.gen(function* () { - const cfg = yield* Config.Service - const fs = yield* AppFileSystem.Service - const http = HttpClient.filterStatusOk(withTransientReadRetry(yield* HttpClient.HttpClient)) +export const layer: Layer.Layer = + Layer.effect( + Service, + Effect.gen(function* () { + const cfg = yield* Config.Service + const fs = yield* AppFileSystem.Service + const http = HttpClient.filterStatusOk(withTransientReadRetry(yield* HttpClient.HttpClient)) - const state = yield* InstanceState.make( - Effect.fn("Instruction.state")(() => - Effect.succeed({ - // Track which instruction files have already been attached for a given assistant message. - claims: new Map>(), - }), - ), - ) + const state = yield* InstanceState.make( + Effect.fn("Instruction.state")(() => + Effect.succeed({ + // Track which instruction files have already been attached for a given assistant message. + claims: new Map>(), + }), + ), + ) - const relative = Effect.fnUntraced(function* (instruction: string) { - if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) { - return yield* fs - .globUp(instruction, Instance.directory, Instance.worktree) - .pipe(Effect.catch(() => Effect.succeed([] as string[]))) - } - if (!Flag.OPENCODE_CONFIG_DIR) { - log.warn( - `Skipping relative instruction "${instruction}" - no OPENCODE_CONFIG_DIR set while project config is disabled`, - ) - return [] - } + const relative = Effect.fnUntraced(function* (instruction: string) { + if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) { return yield* fs - .globUp(instruction, Flag.OPENCODE_CONFIG_DIR, Flag.OPENCODE_CONFIG_DIR) + .globUp(instruction, Instance.directory, Instance.worktree) .pipe(Effect.catch(() => Effect.succeed([] as string[]))) - }) - - const read = Effect.fnUntraced(function* (filepath: string) { - return yield* fs.readFileString(filepath).pipe(Effect.catch(() => Effect.succeed(""))) - }) - - const fetch = Effect.fnUntraced(function* (url: string) { - const res = yield* http.execute(HttpClientRequest.get(url)).pipe( - Effect.timeout(5000), - Effect.catch(() => Effect.succeed(null)), + } + if (!Flag.OPENCODE_CONFIG_DIR) { + log.warn( + `Skipping relative instruction "${instruction}" - no OPENCODE_CONFIG_DIR set while project config is disabled`, ) - if (!res) return "" - const body = yield* res.arrayBuffer.pipe(Effect.catch(() => Effect.succeed(new ArrayBuffer(0)))) - return new TextDecoder().decode(body) - }) + return [] + } + return yield* fs + .globUp(instruction, Flag.OPENCODE_CONFIG_DIR, Flag.OPENCODE_CONFIG_DIR) + .pipe(Effect.catch(() => Effect.succeed([] as string[]))) + }) - const clear = Effect.fn("Instruction.clear")(function* (messageID: MessageID) { - const s = yield* InstanceState.get(state) - s.claims.delete(messageID) - }) + const read = Effect.fnUntraced(function* (filepath: string) { + return yield* fs.readFileString(filepath).pipe(Effect.catch(() => Effect.succeed(""))) + }) - const systemPaths = Effect.fn("Instruction.systemPaths")(function* () { - const config = yield* cfg.get() - const paths = new Set() + const fetch = Effect.fnUntraced(function* (url: string) { + const res = yield* http.execute(HttpClientRequest.get(url)).pipe( + Effect.timeout(5000), + Effect.catch(() => Effect.succeed(null)), + ) + if (!res) return "" + const body = yield* res.arrayBuffer.pipe(Effect.catch(() => Effect.succeed(new ArrayBuffer(0)))) + return new TextDecoder().decode(body) + }) - // The first project-level match wins so we don't stack AGENTS.md/CLAUDE.md from every ancestor. - if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) { - for (const file of FILES) { - const matches = yield* fs.findUp(file, Instance.directory, Instance.worktree) - if (matches.length > 0) { - matches.forEach((item) => paths.add(path.resolve(item))) - break - } - } - } + const clear = Effect.fn("Instruction.clear")(function* (messageID: MessageID) { + const s = yield* InstanceState.get(state) + s.claims.delete(messageID) + }) - for (const file of globalFiles()) { - if (yield* fs.existsSafe(file)) { - paths.add(path.resolve(file)) + const systemPaths = Effect.fn("Instruction.systemPaths")(function* () { + const config = yield* cfg.get() + const paths = new Set() + + // The first project-level match wins so we don't stack AGENTS.md/CLAUDE.md from every ancestor. + if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) { + for (const file of FILES) { + const matches = yield* fs.findUp(file, Instance.directory, Instance.worktree) + if (matches.length > 0) { + matches.forEach((item) => paths.add(path.resolve(item))) break } } + } - if (config.instructions) { - for (const raw of config.instructions) { - if (raw.startsWith("https://") || raw.startsWith("http://")) continue - const instruction = raw.startsWith("~/") ? path.join(os.homedir(), raw.slice(2)) : raw - const matches = yield* ( - path.isAbsolute(instruction) - ? fs.glob(path.basename(instruction), { - cwd: path.dirname(instruction), - absolute: true, - include: "file", - }) - : relative(instruction) - ).pipe(Effect.catch(() => Effect.succeed([] as string[]))) - matches.forEach((item) => paths.add(path.resolve(item))) - } + for (const file of globalFiles()) { + if (yield* fs.existsSafe(file)) { + paths.add(path.resolve(file)) + break } + } - return paths - }) - - const system = Effect.fn("Instruction.system")(function* () { - const config = yield* cfg.get() - const paths = yield* systemPaths() - const urls = (config.instructions ?? []).filter( - (item) => item.startsWith("https://") || item.startsWith("http://"), - ) - - const files = yield* Effect.forEach(Array.from(paths), read, { concurrency: 8 }) - const remote = yield* Effect.forEach(urls, fetch, { concurrency: 4 }) - - return [ - ...Array.from(paths).flatMap((item, i) => (files[i] ? [`Instructions from: ${item}\n${files[i]}`] : [])), - ...urls.flatMap((item, i) => (remote[i] ? [`Instructions from: ${item}\n${remote[i]}`] : [])), - ] - }) - - const find = Effect.fn("Instruction.find")(function* (dir: string) { - for (const file of FILES) { - const filepath = path.resolve(path.join(dir, file)) - if (yield* fs.existsSafe(filepath)) return filepath + if (config.instructions) { + for (const raw of config.instructions) { + if (raw.startsWith("https://") || raw.startsWith("http://")) continue + const instruction = raw.startsWith("~/") ? path.join(os.homedir(), raw.slice(2)) : raw + const matches = yield* ( + path.isAbsolute(instruction) + ? fs.glob(path.basename(instruction), { + cwd: path.dirname(instruction), + absolute: true, + include: "file", + }) + : relative(instruction) + ).pipe(Effect.catch(() => Effect.succeed([] as string[]))) + matches.forEach((item) => paths.add(path.resolve(item))) } - }) + } - const resolve = Effect.fn("Instruction.resolve")(function* ( - messages: MessageV2.WithParts[], - filepath: string, - messageID: MessageID, - ) { - const sys = yield* systemPaths() - const already = extract(messages) - const results: { filepath: string; content: string }[] = [] - const s = yield* InstanceState.get(state) + return paths + }) - const target = path.resolve(filepath) - const root = path.resolve(Instance.directory) - let current = path.dirname(target) + const system = Effect.fn("Instruction.system")(function* () { + const config = yield* cfg.get() + const paths = yield* systemPaths() + const urls = (config.instructions ?? []).filter( + (item) => item.startsWith("https://") || item.startsWith("http://"), + ) - // Walk upward from the file being read and attach nearby instruction files once per message. - while (current.startsWith(root) && current !== root) { - const found = yield* find(current) - if (!found || found === target || sys.has(found) || already.has(found)) { - current = path.dirname(current) - continue - } + const files = yield* Effect.forEach(Array.from(paths), read, { concurrency: 8 }) + const remote = yield* Effect.forEach(urls, fetch, { concurrency: 4 }) - let set = s.claims.get(messageID) - if (!set) { - set = new Set() - s.claims.set(messageID, set) - } - if (set.has(found)) { - current = path.dirname(current) - continue - } + return [ + ...Array.from(paths).flatMap((item, i) => (files[i] ? [`Instructions from: ${item}\n${files[i]}`] : [])), + ...urls.flatMap((item, i) => (remote[i] ? [`Instructions from: ${item}\n${remote[i]}`] : [])), + ] + }) - set.add(found) - const content = yield* read(found) - if (content) { - results.push({ filepath: found, content: `Instructions from: ${found}\n${content}` }) - } + const find = Effect.fn("Instruction.find")(function* (dir: string) { + for (const file of FILES) { + const filepath = path.resolve(path.join(dir, file)) + if (yield* fs.existsSafe(filepath)) return filepath + } + }) + const resolve = Effect.fn("Instruction.resolve")(function* ( + messages: MessageV2.WithParts[], + filepath: string, + messageID: MessageID, + ) { + const sys = yield* systemPaths() + const already = extract(messages) + const results: { filepath: string; content: string }[] = [] + const s = yield* InstanceState.get(state) + + const target = path.resolve(filepath) + const root = path.resolve(Instance.directory) + let current = path.dirname(target) + + // Walk upward from the file being read and attach nearby instruction files once per message. + while (current.startsWith(root) && current !== root) { + const found = yield* find(current) + if (!found || found === target || sys.has(found) || already.has(found)) { current = path.dirname(current) + continue } - return results - }) + let set = s.claims.get(messageID) + if (!set) { + set = new Set() + s.claims.set(messageID, set) + } + if (set.has(found)) { + current = path.dirname(current) + continue + } - return Service.of({ clear, systemPaths, system, find, resolve }) - }), - ) + set.add(found) + const content = yield* read(found) + if (content) { + results.push({ filepath: found, content: `Instructions from: ${found}\n${content}` }) + } - export const defaultLayer = layer.pipe( - Layer.provide(Config.defaultLayer), - Layer.provide(AppFileSystem.defaultLayer), - Layer.provide(FetchHttpClient.layer), + current = path.dirname(current) + } + + return results + }) + + return Service.of({ clear, systemPaths, system, find, resolve }) + }), ) - export function loaded(messages: MessageV2.WithParts[]) { - return extract(messages) - } +export const defaultLayer = layer.pipe( + Layer.provide(Config.defaultLayer), + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(FetchHttpClient.layer), +) + +export function loaded(messages: MessageV2.WithParts[]) { + return extract(messages) } + +export * as Instruction from "./instruction" diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index d38c29765a..b66e99fc82 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -25,429 +25,429 @@ import { EffectBridge } from "@/effect" import * as Option from "effect/Option" import * as OtelTracer from "@effect/opentelemetry/Tracer" -export namespace LLM { - const log = Log.create({ service: "llm" }) - export const OUTPUT_TOKEN_MAX = ProviderTransform.OUTPUT_TOKEN_MAX - type Result = Awaited> +const log = Log.create({ service: "llm" }) +export const OUTPUT_TOKEN_MAX = ProviderTransform.OUTPUT_TOKEN_MAX +type Result = Awaited> - export type StreamInput = { - user: MessageV2.User - sessionID: string - parentSessionID?: string - model: Provider.Model - agent: Agent.Info - permission?: Permission.Ruleset - system: string[] - messages: ModelMessage[] - small?: boolean - tools: Record - retries?: number - toolChoice?: "auto" | "required" | "none" - } +export type StreamInput = { + user: MessageV2.User + sessionID: string + parentSessionID?: string + model: Provider.Model + agent: Agent.Info + permission?: Permission.Ruleset + system: string[] + messages: ModelMessage[] + small?: boolean + tools: Record + retries?: number + toolChoice?: "auto" | "required" | "none" +} - export type StreamRequest = StreamInput & { - abort: AbortSignal - } +export type StreamRequest = StreamInput & { + abort: AbortSignal +} - export type Event = Result["fullStream"] extends AsyncIterable ? T : never +export type Event = Result["fullStream"] extends AsyncIterable ? T : never - export interface Interface { - readonly stream: (input: StreamInput) => Stream.Stream - } +export interface Interface { + readonly stream: (input: StreamInput) => Stream.Stream +} - export class Service extends Context.Service()("@opencode/LLM") {} +export class Service extends Context.Service()("@opencode/LLM") {} - const live: Layer.Layer< - Service, - never, - Auth.Service | Config.Service | Provider.Service | Plugin.Service | Permission.Service - > = Layer.effect( - Service, - Effect.gen(function* () { - const auth = yield* Auth.Service - const config = yield* Config.Service - const provider = yield* Provider.Service - const plugin = yield* Plugin.Service - const perm = yield* Permission.Service +const live: Layer.Layer< + Service, + never, + Auth.Service | Config.Service | Provider.Service | Plugin.Service | Permission.Service +> = Layer.effect( + Service, + Effect.gen(function* () { + const auth = yield* Auth.Service + const config = yield* Config.Service + const provider = yield* Provider.Service + const plugin = yield* Plugin.Service + const perm = yield* Permission.Service - const run = Effect.fn("LLM.run")(function* (input: StreamRequest) { - const l = log - .clone() - .tag("providerID", input.model.providerID) - .tag("modelID", input.model.id) - .tag("sessionID", input.sessionID) - .tag("small", (input.small ?? false).toString()) - .tag("agent", input.agent.name) - .tag("mode", input.agent.mode) - l.info("stream", { - modelID: input.model.id, - providerID: input.model.providerID, - }) - - const [language, cfg, item, info] = yield* Effect.all( - [ - provider.getLanguage(input.model), - config.get(), - provider.getProvider(input.model.providerID), - auth.get(input.model.providerID), - ], - { concurrency: "unbounded" }, - ) - - // TODO: move this to a proper hook - const isOpenaiOauth = item.id === "openai" && info?.type === "oauth" - - const system: string[] = [] - system.push( - [ - // use agent prompt otherwise provider prompt - ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)), - // any custom prompt passed into this call - ...input.system, - // any custom prompt from last user message - ...(input.user.system ? [input.user.system] : []), - ] - .filter((x) => x) - .join("\n"), - ) - - const header = system[0] - yield* plugin.trigger( - "experimental.chat.system.transform", - { sessionID: input.sessionID, model: input.model }, - { system }, - ) - // rejoin to maintain 2-part structure for caching if header unchanged - if (system.length > 2 && system[0] === header) { - const rest = system.slice(1) - system.length = 0 - system.push(header, rest.join("\n")) - } - - const variant = - !input.small && input.model.variants && input.user.model.variant - ? input.model.variants[input.user.model.variant] - : {} - const base = input.small - ? ProviderTransform.smallOptions(input.model) - : ProviderTransform.options({ - model: input.model, - sessionID: input.sessionID, - providerOptions: item.options, - }) - const options: Record = pipe( - base, - mergeDeep(input.model.options), - mergeDeep(input.agent.options), - mergeDeep(variant), - ) - if (isOpenaiOauth) { - options.instructions = system.join("\n") - } - - const isWorkflow = language instanceof GitLabWorkflowLanguageModel - const messages = isOpenaiOauth - ? input.messages - : isWorkflow - ? input.messages - : [ - ...system.map( - (x): ModelMessage => ({ - role: "system", - content: x, - }), - ), - ...input.messages, - ] - - const params = yield* plugin.trigger( - "chat.params", - { - sessionID: input.sessionID, - agent: input.agent.name, - model: input.model, - provider: item, - message: input.user, - }, - { - temperature: input.model.capabilities.temperature - ? (input.agent.temperature ?? ProviderTransform.temperature(input.model)) - : undefined, - topP: input.agent.topP ?? ProviderTransform.topP(input.model), - topK: ProviderTransform.topK(input.model), - maxOutputTokens: ProviderTransform.maxOutputTokens(input.model), - options, - }, - ) - - const { headers } = yield* plugin.trigger( - "chat.headers", - { - sessionID: input.sessionID, - agent: input.agent.name, - model: input.model, - provider: item, - message: input.user, - }, - { - headers: {}, - }, - ) - - const tools = resolveTools(input) - - // LiteLLM and some Anthropic proxies require the tools parameter to be present - // when message history contains tool calls, even if no tools are being used. - // Add a dummy tool that is never called to satisfy this validation. - // This is enabled for: - // 1. Providers with "litellm" in their ID or API ID (auto-detected) - // 2. Providers with explicit "litellmProxy: true" option (opt-in for custom gateways) - const isLiteLLMProxy = - item.options?.["litellmProxy"] === true || - input.model.providerID.toLowerCase().includes("litellm") || - input.model.api.id.toLowerCase().includes("litellm") - - // LiteLLM/Bedrock rejects requests where the message history contains tool - // calls but no tools param is present. When there are no active tools (e.g. - // during compaction), inject a stub tool to satisfy the validation requirement. - // The stub description explicitly tells the model not to call it. - if ( - (isLiteLLMProxy || input.model.providerID.includes("github-copilot")) && - Object.keys(tools).length === 0 && - hasToolCalls(input.messages) - ) { - tools["_noop"] = tool({ - description: "Do not call this tool. It exists only for API compatibility and must never be invoked.", - inputSchema: jsonSchema({ - type: "object", - properties: { - reason: { type: "string", description: "Unused" }, - }, - }), - execute: async () => ({ output: "", title: "", metadata: {} }), - }) - } - - // Wire up toolExecutor for DWS workflow models so that tool calls - // from the workflow service are executed via opencode's tool system - // and results sent back over the WebSocket. - if (language instanceof GitLabWorkflowLanguageModel) { - const workflowModel = language as GitLabWorkflowLanguageModel & { - sessionID?: string - sessionPreapprovedTools?: string[] - approvalHandler?: (approvalTools: { name: string; args: string }[]) => Promise<{ approved: boolean }> - } - workflowModel.sessionID = input.sessionID - workflowModel.systemPrompt = system.join("\n") - workflowModel.toolExecutor = async (toolName, argsJson, _requestID) => { - const t = tools[toolName] - if (!t || !t.execute) { - return { result: "", error: `Unknown tool: ${toolName}` } - } - try { - const result = await t.execute!(JSON.parse(argsJson), { - toolCallId: _requestID, - messages: input.messages, - abortSignal: input.abort, - }) - const output = typeof result === "string" ? result : (result?.output ?? JSON.stringify(result)) - return { - result: output, - metadata: typeof result === "object" ? result?.metadata : undefined, - title: typeof result === "object" ? result?.title : undefined, - } - } catch (e: any) { - return { result: "", error: e.message ?? String(e) } - } - } - - const ruleset = Permission.merge(input.agent.permission ?? [], input.permission ?? []) - workflowModel.sessionPreapprovedTools = Object.keys(tools).filter((name) => { - const match = ruleset.findLast((rule) => Wildcard.match(name, rule.permission)) - return !match || match.action !== "ask" - }) - - const bridge = yield* EffectBridge.make() - const approvedToolsForSession = new Set() - workflowModel.approvalHandler = Instance.bind(async (approvalTools) => { - const uniqueNames = [...new Set(approvalTools.map((t: { name: string }) => t.name))] as string[] - // Auto-approve tools that were already approved in this session - // (prevents infinite approval loops for server-side MCP tools) - if (uniqueNames.every((name) => approvedToolsForSession.has(name))) { - return { approved: true } - } - - const id = PermissionID.ascending() - let unsub: (() => void) | undefined - try { - unsub = Bus.subscribe(Permission.Event.Replied, (evt) => { - if (evt.properties.requestID === id) void evt.properties.reply - }) - const toolPatterns = approvalTools.map((t: { name: string; args: string }) => { - try { - const parsed = JSON.parse(t.args) as Record - const title = (parsed?.title ?? parsed?.name ?? "") as string - return title ? `${t.name}: ${title}` : t.name - } catch { - return t.name - } - }) - const uniquePatterns = [...new Set(toolPatterns)] as string[] - await bridge.promise( - perm.ask({ - id, - sessionID: SessionID.make(input.sessionID), - permission: "workflow_tool_approval", - patterns: uniquePatterns, - metadata: { tools: approvalTools }, - always: uniquePatterns, - ruleset: [], - }), - ) - for (const name of uniqueNames) approvedToolsForSession.add(name) - workflowModel.sessionPreapprovedTools = [...(workflowModel.sessionPreapprovedTools ?? []), ...uniqueNames] - return { approved: true } - } catch { - return { approved: false } - } finally { - unsub?.() - } - }) - } - - const tracer = cfg.experimental?.openTelemetry - ? Option.getOrUndefined(yield* Effect.serviceOption(OtelTracer.OtelTracer)) - : undefined - - return streamText({ - onError(error) { - l.error("stream error", { - error, - }) - }, - async experimental_repairToolCall(failed) { - const lower = failed.toolCall.toolName.toLowerCase() - if (lower !== failed.toolCall.toolName && tools[lower]) { - l.info("repairing tool call", { - tool: failed.toolCall.toolName, - repaired: lower, - }) - return { - ...failed.toolCall, - toolName: lower, - } - } - return { - ...failed.toolCall, - input: JSON.stringify({ - tool: failed.toolCall.toolName, - error: failed.error.message, - }), - toolName: "invalid", - } - }, - temperature: params.temperature, - topP: params.topP, - topK: params.topK, - providerOptions: ProviderTransform.providerOptions(input.model, params.options), - activeTools: Object.keys(tools).filter((x) => x !== "invalid"), - tools, - toolChoice: input.toolChoice, - maxOutputTokens: params.maxOutputTokens, - abortSignal: input.abort, - headers: { - ...(input.model.providerID.startsWith("opencode") - ? { - "x-opencode-project": Instance.project.id, - "x-opencode-session": input.sessionID, - "x-opencode-request": input.user.id, - "x-opencode-client": Flag.OPENCODE_CLIENT, - } - : { - "x-session-affinity": input.sessionID, - ...(input.parentSessionID ? { "x-parent-session-id": input.parentSessionID } : {}), - "User-Agent": `opencode/${InstallationVersion}`, - }), - ...input.model.headers, - ...headers, - }, - maxRetries: input.retries ?? 0, - messages, - model: wrapLanguageModel({ - model: language, - middleware: [ - { - specificationVersion: "v3" as const, - async transformParams(args) { - if (args.type === "stream") { - // @ts-expect-error - args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, options) - } - return args.params - }, - }, - ], - }), - experimental_telemetry: { - isEnabled: cfg.experimental?.openTelemetry, - functionId: "session.llm", - tracer, - metadata: { - userId: cfg.username ?? "unknown", - sessionId: input.sessionID, - }, - }, - }) + const run = Effect.fn("LLM.run")(function* (input: StreamRequest) { + const l = log + .clone() + .tag("providerID", input.model.providerID) + .tag("modelID", input.model.id) + .tag("sessionID", input.sessionID) + .tag("small", (input.small ?? false).toString()) + .tag("agent", input.agent.name) + .tag("mode", input.agent.mode) + l.info("stream", { + modelID: input.model.id, + providerID: input.model.providerID, }) - const stream: Interface["stream"] = (input) => - Stream.scoped( - Stream.unwrap( - Effect.gen(function* () { - const ctrl = yield* Effect.acquireRelease( - Effect.sync(() => new AbortController()), - (ctrl) => Effect.sync(() => ctrl.abort()), - ) + const [language, cfg, item, info] = yield* Effect.all( + [ + provider.getLanguage(input.model), + config.get(), + provider.getProvider(input.model.providerID), + auth.get(input.model.providerID), + ], + { concurrency: "unbounded" }, + ) - const result = yield* run({ ...input, abort: ctrl.signal }) + // TODO: move this to a proper hook + const isOpenaiOauth = item.id === "openai" && info?.type === "oauth" - return Stream.fromAsyncIterable(result.fullStream, (e) => (e instanceof Error ? e : new Error(String(e)))) - }), - ), - ) + const system: string[] = [] + system.push( + [ + // use agent prompt otherwise provider prompt + ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)), + // any custom prompt passed into this call + ...input.system, + // any custom prompt from last user message + ...(input.user.system ? [input.user.system] : []), + ] + .filter((x) => x) + .join("\n"), + ) - return Service.of({ stream }) - }), - ) - - export const layer = live.pipe(Layer.provide(Permission.defaultLayer)) - - export const defaultLayer = Layer.suspend(() => - layer.pipe( - Layer.provide(Auth.defaultLayer), - Layer.provide(Config.defaultLayer), - Layer.provide(Provider.defaultLayer), - Layer.provide(Plugin.defaultLayer), - ), - ) - - function resolveTools(input: Pick) { - const disabled = Permission.disabled( - Object.keys(input.tools), - Permission.merge(input.agent.permission, input.permission ?? []), - ) - return Record.filter(input.tools, (_, k) => input.user.tools?.[k] !== false && !disabled.has(k)) - } - - // Check if messages contain any tool-call content - // Used to determine if a dummy tool should be added for LiteLLM proxy compatibility - export function hasToolCalls(messages: ModelMessage[]): boolean { - for (const msg of messages) { - if (!Array.isArray(msg.content)) continue - for (const part of msg.content) { - if (part.type === "tool-call" || part.type === "tool-result") return true + const header = system[0] + yield* plugin.trigger( + "experimental.chat.system.transform", + { sessionID: input.sessionID, model: input.model }, + { system }, + ) + // rejoin to maintain 2-part structure for caching if header unchanged + if (system.length > 2 && system[0] === header) { + const rest = system.slice(1) + system.length = 0 + system.push(header, rest.join("\n")) } - } - return false - } + + const variant = + !input.small && input.model.variants && input.user.model.variant + ? input.model.variants[input.user.model.variant] + : {} + const base = input.small + ? ProviderTransform.smallOptions(input.model) + : ProviderTransform.options({ + model: input.model, + sessionID: input.sessionID, + providerOptions: item.options, + }) + const options: Record = pipe( + base, + mergeDeep(input.model.options), + mergeDeep(input.agent.options), + mergeDeep(variant), + ) + if (isOpenaiOauth) { + options.instructions = system.join("\n") + } + + const isWorkflow = language instanceof GitLabWorkflowLanguageModel + const messages = isOpenaiOauth + ? input.messages + : isWorkflow + ? input.messages + : [ + ...system.map( + (x): ModelMessage => ({ + role: "system", + content: x, + }), + ), + ...input.messages, + ] + + const params = yield* plugin.trigger( + "chat.params", + { + sessionID: input.sessionID, + agent: input.agent.name, + model: input.model, + provider: item, + message: input.user, + }, + { + temperature: input.model.capabilities.temperature + ? (input.agent.temperature ?? ProviderTransform.temperature(input.model)) + : undefined, + topP: input.agent.topP ?? ProviderTransform.topP(input.model), + topK: ProviderTransform.topK(input.model), + maxOutputTokens: ProviderTransform.maxOutputTokens(input.model), + options, + }, + ) + + const { headers } = yield* plugin.trigger( + "chat.headers", + { + sessionID: input.sessionID, + agent: input.agent.name, + model: input.model, + provider: item, + message: input.user, + }, + { + headers: {}, + }, + ) + + const tools = resolveTools(input) + + // LiteLLM and some Anthropic proxies require the tools parameter to be present + // when message history contains tool calls, even if no tools are being used. + // Add a dummy tool that is never called to satisfy this validation. + // This is enabled for: + // 1. Providers with "litellm" in their ID or API ID (auto-detected) + // 2. Providers with explicit "litellmProxy: true" option (opt-in for custom gateways) + const isLiteLLMProxy = + item.options?.["litellmProxy"] === true || + input.model.providerID.toLowerCase().includes("litellm") || + input.model.api.id.toLowerCase().includes("litellm") + + // LiteLLM/Bedrock rejects requests where the message history contains tool + // calls but no tools param is present. When there are no active tools (e.g. + // during compaction), inject a stub tool to satisfy the validation requirement. + // The stub description explicitly tells the model not to call it. + if ( + (isLiteLLMProxy || input.model.providerID.includes("github-copilot")) && + Object.keys(tools).length === 0 && + hasToolCalls(input.messages) + ) { + tools["_noop"] = tool({ + description: "Do not call this tool. It exists only for API compatibility and must never be invoked.", + inputSchema: jsonSchema({ + type: "object", + properties: { + reason: { type: "string", description: "Unused" }, + }, + }), + execute: async () => ({ output: "", title: "", metadata: {} }), + }) + } + + // Wire up toolExecutor for DWS workflow models so that tool calls + // from the workflow service are executed via opencode's tool system + // and results sent back over the WebSocket. + if (language instanceof GitLabWorkflowLanguageModel) { + const workflowModel = language as GitLabWorkflowLanguageModel & { + sessionID?: string + sessionPreapprovedTools?: string[] + approvalHandler?: (approvalTools: { name: string; args: string }[]) => Promise<{ approved: boolean }> + } + workflowModel.sessionID = input.sessionID + workflowModel.systemPrompt = system.join("\n") + workflowModel.toolExecutor = async (toolName, argsJson, _requestID) => { + const t = tools[toolName] + if (!t || !t.execute) { + return { result: "", error: `Unknown tool: ${toolName}` } + } + try { + const result = await t.execute!(JSON.parse(argsJson), { + toolCallId: _requestID, + messages: input.messages, + abortSignal: input.abort, + }) + const output = typeof result === "string" ? result : (result?.output ?? JSON.stringify(result)) + return { + result: output, + metadata: typeof result === "object" ? result?.metadata : undefined, + title: typeof result === "object" ? result?.title : undefined, + } + } catch (e: any) { + return { result: "", error: e.message ?? String(e) } + } + } + + const ruleset = Permission.merge(input.agent.permission ?? [], input.permission ?? []) + workflowModel.sessionPreapprovedTools = Object.keys(tools).filter((name) => { + const match = ruleset.findLast((rule) => Wildcard.match(name, rule.permission)) + return !match || match.action !== "ask" + }) + + const bridge = yield* EffectBridge.make() + const approvedToolsForSession = new Set() + workflowModel.approvalHandler = Instance.bind(async (approvalTools) => { + const uniqueNames = [...new Set(approvalTools.map((t: { name: string }) => t.name))] as string[] + // Auto-approve tools that were already approved in this session + // (prevents infinite approval loops for server-side MCP tools) + if (uniqueNames.every((name) => approvedToolsForSession.has(name))) { + return { approved: true } + } + + const id = PermissionID.ascending() + let unsub: (() => void) | undefined + try { + unsub = Bus.subscribe(Permission.Event.Replied, (evt) => { + if (evt.properties.requestID === id) void evt.properties.reply + }) + const toolPatterns = approvalTools.map((t: { name: string; args: string }) => { + try { + const parsed = JSON.parse(t.args) as Record + const title = (parsed?.title ?? parsed?.name ?? "") as string + return title ? `${t.name}: ${title}` : t.name + } catch { + return t.name + } + }) + const uniquePatterns = [...new Set(toolPatterns)] as string[] + await bridge.promise( + perm.ask({ + id, + sessionID: SessionID.make(input.sessionID), + permission: "workflow_tool_approval", + patterns: uniquePatterns, + metadata: { tools: approvalTools }, + always: uniquePatterns, + ruleset: [], + }), + ) + for (const name of uniqueNames) approvedToolsForSession.add(name) + workflowModel.sessionPreapprovedTools = [...(workflowModel.sessionPreapprovedTools ?? []), ...uniqueNames] + return { approved: true } + } catch { + return { approved: false } + } finally { + unsub?.() + } + }) + } + + const tracer = cfg.experimental?.openTelemetry + ? Option.getOrUndefined(yield* Effect.serviceOption(OtelTracer.OtelTracer)) + : undefined + + return streamText({ + onError(error) { + l.error("stream error", { + error, + }) + }, + async experimental_repairToolCall(failed) { + const lower = failed.toolCall.toolName.toLowerCase() + if (lower !== failed.toolCall.toolName && tools[lower]) { + l.info("repairing tool call", { + tool: failed.toolCall.toolName, + repaired: lower, + }) + return { + ...failed.toolCall, + toolName: lower, + } + } + return { + ...failed.toolCall, + input: JSON.stringify({ + tool: failed.toolCall.toolName, + error: failed.error.message, + }), + toolName: "invalid", + } + }, + temperature: params.temperature, + topP: params.topP, + topK: params.topK, + providerOptions: ProviderTransform.providerOptions(input.model, params.options), + activeTools: Object.keys(tools).filter((x) => x !== "invalid"), + tools, + toolChoice: input.toolChoice, + maxOutputTokens: params.maxOutputTokens, + abortSignal: input.abort, + headers: { + ...(input.model.providerID.startsWith("opencode") + ? { + "x-opencode-project": Instance.project.id, + "x-opencode-session": input.sessionID, + "x-opencode-request": input.user.id, + "x-opencode-client": Flag.OPENCODE_CLIENT, + } + : { + "x-session-affinity": input.sessionID, + ...(input.parentSessionID ? { "x-parent-session-id": input.parentSessionID } : {}), + "User-Agent": `opencode/${InstallationVersion}`, + }), + ...input.model.headers, + ...headers, + }, + maxRetries: input.retries ?? 0, + messages, + model: wrapLanguageModel({ + model: language, + middleware: [ + { + specificationVersion: "v3" as const, + async transformParams(args) { + if (args.type === "stream") { + // @ts-expect-error + args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, options) + } + return args.params + }, + }, + ], + }), + experimental_telemetry: { + isEnabled: cfg.experimental?.openTelemetry, + functionId: "session.llm", + tracer, + metadata: { + userId: cfg.username ?? "unknown", + sessionId: input.sessionID, + }, + }, + }) + }) + + const stream: Interface["stream"] = (input) => + Stream.scoped( + Stream.unwrap( + Effect.gen(function* () { + const ctrl = yield* Effect.acquireRelease( + Effect.sync(() => new AbortController()), + (ctrl) => Effect.sync(() => ctrl.abort()), + ) + + const result = yield* run({ ...input, abort: ctrl.signal }) + + return Stream.fromAsyncIterable(result.fullStream, (e) => (e instanceof Error ? e : new Error(String(e)))) + }), + ), + ) + + return Service.of({ stream }) + }), +) + +export const layer = live.pipe(Layer.provide(Permission.defaultLayer)) + +export const defaultLayer = Layer.suspend(() => + layer.pipe( + Layer.provide(Auth.defaultLayer), + Layer.provide(Config.defaultLayer), + Layer.provide(Provider.defaultLayer), + Layer.provide(Plugin.defaultLayer), + ), +) + +function resolveTools(input: Pick) { + const disabled = Permission.disabled( + Object.keys(input.tools), + Permission.merge(input.agent.permission, input.permission ?? []), + ) + return Record.filter(input.tools, (_, k) => input.user.tools?.[k] !== false && !disabled.has(k)) } + +// Check if messages contain any tool-call content +// Used to determine if a dummy tool should be added for LiteLLM proxy compatibility +export function hasToolCalls(messages: ModelMessage[]): boolean { + for (const msg of messages) { + if (!Array.isArray(msg.content)) continue + for (const part of msg.content) { + if (part.type === "tool-call" || part.type === "tool-result") return true + } + } + return false +} + +export * as LLM from "./llm" diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index f5ba74826d..5e7e008401 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -24,726 +24,738 @@ interface FetchDecompressionError extends Error { path: string } -export namespace MessageV2 { - export const SYNTHETIC_ATTACHMENT_PROMPT = "Attached image(s) from tool result:" +export const SYNTHETIC_ATTACHMENT_PROMPT = "Attached image(s) from tool result:" - export function isMedia(mime: string) { - return mime.startsWith("image/") || mime === "application/pdf" - } +export function isMedia(mime: string) { + return mime.startsWith("image/") || mime === "application/pdf" +} - export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({})) - export const AbortedError = NamedError.create("MessageAbortedError", z.object({ message: z.string() })) - export const StructuredOutputError = NamedError.create( - "StructuredOutputError", - z.object({ - message: z.string(), - retries: z.number(), - }), - ) - export const AuthError = NamedError.create( - "ProviderAuthError", - z.object({ - providerID: z.string(), - message: z.string(), - }), - ) - export const APIError = NamedError.create( - "APIError", - z.object({ - message: z.string(), - statusCode: z.number().optional(), - isRetryable: z.boolean(), - responseHeaders: z.record(z.string(), z.string()).optional(), - responseBody: z.string().optional(), - metadata: z.record(z.string(), z.string()).optional(), - }), - ) - export type APIError = z.infer - export const ContextOverflowError = NamedError.create( - "ContextOverflowError", - z.object({ message: z.string(), responseBody: z.string().optional() }), - ) +export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({})) +export const AbortedError = NamedError.create("MessageAbortedError", z.object({ message: z.string() })) +export const StructuredOutputError = NamedError.create( + "StructuredOutputError", + z.object({ + message: z.string(), + retries: z.number(), + }), +) +export const AuthError = NamedError.create( + "ProviderAuthError", + z.object({ + providerID: z.string(), + message: z.string(), + }), +) +export const APIError = NamedError.create( + "APIError", + z.object({ + message: z.string(), + statusCode: z.number().optional(), + isRetryable: z.boolean(), + responseHeaders: z.record(z.string(), z.string()).optional(), + responseBody: z.string().optional(), + metadata: z.record(z.string(), z.string()).optional(), + }), +) +export type APIError = z.infer +export const ContextOverflowError = NamedError.create( + "ContextOverflowError", + z.object({ message: z.string(), responseBody: z.string().optional() }), +) - export const OutputFormatText = z - .object({ - type: z.literal("text"), - }) - .meta({ - ref: "OutputFormatText", - }) - - export const OutputFormatJsonSchema = z - .object({ - type: z.literal("json_schema"), - schema: z.record(z.string(), z.any()).meta({ ref: "JSONSchema" }), - retryCount: z.number().int().min(0).default(2), - }) - .meta({ - ref: "OutputFormatJsonSchema", - }) - - export const Format = z.discriminatedUnion("type", [OutputFormatText, OutputFormatJsonSchema]).meta({ - ref: "OutputFormat", - }) - export type OutputFormat = z.infer - - const PartBase = z.object({ - id: PartID.zod, - sessionID: SessionID.zod, - messageID: MessageID.zod, - }) - - export const SnapshotPart = PartBase.extend({ - type: z.literal("snapshot"), - snapshot: z.string(), - }).meta({ - ref: "SnapshotPart", - }) - export type SnapshotPart = z.infer - - export const PatchPart = PartBase.extend({ - type: z.literal("patch"), - hash: z.string(), - files: z.string().array(), - }).meta({ - ref: "PatchPart", - }) - export type PatchPart = z.infer - - export const TextPart = PartBase.extend({ +export const OutputFormatText = z + .object({ type: z.literal("text"), - text: z.string(), - synthetic: z.boolean().optional(), - ignored: z.boolean().optional(), - time: z - .object({ - start: z.number(), - end: z.number().optional(), - }) - .optional(), - metadata: z.record(z.string(), z.any()).optional(), - }).meta({ - ref: "TextPart", }) - export type TextPart = z.infer + .meta({ + ref: "OutputFormatText", + }) - export const ReasoningPart = PartBase.extend({ - type: z.literal("reasoning"), - text: z.string(), +export const OutputFormatJsonSchema = z + .object({ + type: z.literal("json_schema"), + schema: z.record(z.string(), z.any()).meta({ ref: "JSONSchema" }), + retryCount: z.number().int().min(0).default(2), + }) + .meta({ + ref: "OutputFormatJsonSchema", + }) + +export const Format = z.discriminatedUnion("type", [OutputFormatText, OutputFormatJsonSchema]).meta({ + ref: "OutputFormat", +}) +export type OutputFormat = z.infer + +const PartBase = z.object({ + id: PartID.zod, + sessionID: SessionID.zod, + messageID: MessageID.zod, +}) + +export const SnapshotPart = PartBase.extend({ + type: z.literal("snapshot"), + snapshot: z.string(), +}).meta({ + ref: "SnapshotPart", +}) +export type SnapshotPart = z.infer + +export const PatchPart = PartBase.extend({ + type: z.literal("patch"), + hash: z.string(), + files: z.string().array(), +}).meta({ + ref: "PatchPart", +}) +export type PatchPart = z.infer + +export const TextPart = PartBase.extend({ + type: z.literal("text"), + text: z.string(), + synthetic: z.boolean().optional(), + ignored: z.boolean().optional(), + time: z + .object({ + start: z.number(), + end: z.number().optional(), + }) + .optional(), + metadata: z.record(z.string(), z.any()).optional(), +}).meta({ + ref: "TextPart", +}) +export type TextPart = z.infer + +export const ReasoningPart = PartBase.extend({ + type: z.literal("reasoning"), + text: z.string(), + metadata: z.record(z.string(), z.any()).optional(), + time: z.object({ + start: z.number(), + end: z.number().optional(), + }), +}).meta({ + ref: "ReasoningPart", +}) +export type ReasoningPart = z.infer + +const FilePartSourceBase = z.object({ + text: z + .object({ + value: z.string(), + start: z.number().int(), + end: z.number().int(), + }) + .meta({ + ref: "FilePartSourceText", + }), +}) + +export const FileSource = FilePartSourceBase.extend({ + type: z.literal("file"), + path: z.string(), +}).meta({ + ref: "FileSource", +}) + +export const SymbolSource = FilePartSourceBase.extend({ + type: z.literal("symbol"), + path: z.string(), + range: LSP.Range, + name: z.string(), + kind: z.number().int(), +}).meta({ + ref: "SymbolSource", +}) + +export const ResourceSource = FilePartSourceBase.extend({ + type: z.literal("resource"), + clientName: z.string(), + uri: z.string(), +}).meta({ + ref: "ResourceSource", +}) + +export const FilePartSource = z.discriminatedUnion("type", [FileSource, SymbolSource, ResourceSource]).meta({ + ref: "FilePartSource", +}) + +export const FilePart = PartBase.extend({ + type: z.literal("file"), + mime: z.string(), + filename: z.string().optional(), + url: z.string(), + source: FilePartSource.optional(), +}).meta({ + ref: "FilePart", +}) +export type FilePart = z.infer + +export const AgentPart = PartBase.extend({ + type: z.literal("agent"), + name: z.string(), + source: z + .object({ + value: z.string(), + start: z.number().int(), + end: z.number().int(), + }) + .optional(), +}).meta({ + ref: "AgentPart", +}) +export type AgentPart = z.infer + +export const CompactionPart = PartBase.extend({ + type: z.literal("compaction"), + auto: z.boolean(), + overflow: z.boolean().optional(), +}).meta({ + ref: "CompactionPart", +}) +export type CompactionPart = z.infer + +export const SubtaskPart = PartBase.extend({ + type: z.literal("subtask"), + prompt: z.string(), + description: z.string(), + agent: z.string(), + model: z + .object({ + providerID: ProviderID.zod, + modelID: ModelID.zod, + }) + .optional(), + command: z.string().optional(), +}).meta({ + ref: "SubtaskPart", +}) +export type SubtaskPart = z.infer + +export const RetryPart = PartBase.extend({ + type: z.literal("retry"), + attempt: z.number(), + error: APIError.Schema, + time: z.object({ + created: z.number(), + }), +}).meta({ + ref: "RetryPart", +}) +export type RetryPart = z.infer + +export const StepStartPart = PartBase.extend({ + type: z.literal("step-start"), + snapshot: z.string().optional(), +}).meta({ + ref: "StepStartPart", +}) +export type StepStartPart = z.infer + +export const StepFinishPart = PartBase.extend({ + type: z.literal("step-finish"), + reason: z.string(), + snapshot: z.string().optional(), + cost: z.number(), + tokens: z.object({ + total: z.number().optional(), + input: z.number(), + output: z.number(), + reasoning: z.number(), + cache: z.object({ + read: z.number(), + write: z.number(), + }), + }), +}).meta({ + ref: "StepFinishPart", +}) +export type StepFinishPart = z.infer + +export const ToolStatePending = z + .object({ + status: z.literal("pending"), + input: z.record(z.string(), z.any()), + raw: z.string(), + }) + .meta({ + ref: "ToolStatePending", + }) + +export type ToolStatePending = z.infer + +export const ToolStateRunning = z + .object({ + status: z.literal("running"), + input: z.record(z.string(), z.any()), + title: z.string().optional(), metadata: z.record(z.string(), z.any()).optional(), time: z.object({ start: z.number(), - end: z.number().optional(), }), - }).meta({ - ref: "ReasoningPart", }) - export type ReasoningPart = z.infer - - const FilePartSourceBase = z.object({ - text: z - .object({ - value: z.string(), - start: z.number().int(), - end: z.number().int(), - }) - .meta({ - ref: "FilePartSourceText", - }), + .meta({ + ref: "ToolStateRunning", }) +export type ToolStateRunning = z.infer - export const FileSource = FilePartSourceBase.extend({ - type: z.literal("file"), - path: z.string(), - }).meta({ - ref: "FileSource", - }) - - export const SymbolSource = FilePartSourceBase.extend({ - type: z.literal("symbol"), - path: z.string(), - range: LSP.Range, - name: z.string(), - kind: z.number().int(), - }).meta({ - ref: "SymbolSource", - }) - - export const ResourceSource = FilePartSourceBase.extend({ - type: z.literal("resource"), - clientName: z.string(), - uri: z.string(), - }).meta({ - ref: "ResourceSource", - }) - - export const FilePartSource = z.discriminatedUnion("type", [FileSource, SymbolSource, ResourceSource]).meta({ - ref: "FilePartSource", - }) - - export const FilePart = PartBase.extend({ - type: z.literal("file"), - mime: z.string(), - filename: z.string().optional(), - url: z.string(), - source: FilePartSource.optional(), - }).meta({ - ref: "FilePart", - }) - export type FilePart = z.infer - - export const AgentPart = PartBase.extend({ - type: z.literal("agent"), - name: z.string(), - source: z - .object({ - value: z.string(), - start: z.number().int(), - end: z.number().int(), - }) - .optional(), - }).meta({ - ref: "AgentPart", - }) - export type AgentPart = z.infer - - export const CompactionPart = PartBase.extend({ - type: z.literal("compaction"), - auto: z.boolean(), - overflow: z.boolean().optional(), - }).meta({ - ref: "CompactionPart", - }) - export type CompactionPart = z.infer - - export const SubtaskPart = PartBase.extend({ - type: z.literal("subtask"), - prompt: z.string(), - description: z.string(), - agent: z.string(), - model: z - .object({ - providerID: ProviderID.zod, - modelID: ModelID.zod, - }) - .optional(), - command: z.string().optional(), - }).meta({ - ref: "SubtaskPart", - }) - export type SubtaskPart = z.infer - - export const RetryPart = PartBase.extend({ - type: z.literal("retry"), - attempt: z.number(), - error: APIError.Schema, +export const ToolStateCompleted = z + .object({ + status: z.literal("completed"), + input: z.record(z.string(), z.any()), + output: z.string(), + title: z.string(), + metadata: z.record(z.string(), z.any()), time: z.object({ - created: z.number(), + start: z.number(), + end: z.number(), + compacted: z.number().optional(), }), - }).meta({ - ref: "RetryPart", + attachments: FilePart.array().optional(), }) - export type RetryPart = z.infer - - export const StepStartPart = PartBase.extend({ - type: z.literal("step-start"), - snapshot: z.string().optional(), - }).meta({ - ref: "StepStartPart", + .meta({ + ref: "ToolStateCompleted", }) - export type StepStartPart = z.infer +export type ToolStateCompleted = z.infer - export const StepFinishPart = PartBase.extend({ - type: z.literal("step-finish"), - reason: z.string(), - snapshot: z.string().optional(), - cost: z.number(), - tokens: z.object({ - total: z.number().optional(), - input: z.number(), - output: z.number(), - reasoning: z.number(), - cache: z.object({ - read: z.number(), - write: z.number(), - }), - }), - }).meta({ - ref: "StepFinishPart", - }) - export type StepFinishPart = z.infer - - export const ToolStatePending = z - .object({ - status: z.literal("pending"), - input: z.record(z.string(), z.any()), - raw: z.string(), - }) - .meta({ - ref: "ToolStatePending", - }) - - export type ToolStatePending = z.infer - - export const ToolStateRunning = z - .object({ - status: z.literal("running"), - input: z.record(z.string(), z.any()), - title: z.string().optional(), - metadata: z.record(z.string(), z.any()).optional(), - time: z.object({ - start: z.number(), - }), - }) - .meta({ - ref: "ToolStateRunning", - }) - export type ToolStateRunning = z.infer - - export const ToolStateCompleted = z - .object({ - status: z.literal("completed"), - input: z.record(z.string(), z.any()), - output: z.string(), - title: z.string(), - metadata: z.record(z.string(), z.any()), - time: z.object({ - start: z.number(), - end: z.number(), - compacted: z.number().optional(), - }), - attachments: FilePart.array().optional(), - }) - .meta({ - ref: "ToolStateCompleted", - }) - export type ToolStateCompleted = z.infer - - export const ToolStateError = z - .object({ - status: z.literal("error"), - input: z.record(z.string(), z.any()), - error: z.string(), - metadata: z.record(z.string(), z.any()).optional(), - time: z.object({ - start: z.number(), - end: z.number(), - }), - }) - .meta({ - ref: "ToolStateError", - }) - export type ToolStateError = z.infer - - export const ToolState = z - .discriminatedUnion("status", [ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]) - .meta({ - ref: "ToolState", - }) - - export const ToolPart = PartBase.extend({ - type: z.literal("tool"), - callID: z.string(), - tool: z.string(), - state: ToolState, +export const ToolStateError = z + .object({ + status: z.literal("error"), + input: z.record(z.string(), z.any()), + error: z.string(), metadata: z.record(z.string(), z.any()).optional(), - }).meta({ - ref: "ToolPart", - }) - export type ToolPart = z.infer - - const Base = z.object({ - id: MessageID.zod, - sessionID: SessionID.zod, - }) - - export const User = Base.extend({ - role: z.literal("user"), time: z.object({ - created: z.number(), + start: z.number(), + end: z.number(), }), - format: Format.optional(), - summary: z - .object({ - title: z.string().optional(), - body: z.string().optional(), - diffs: Snapshot.FileDiff.array(), - }) - .optional(), - agent: z.string(), - model: z.object({ - providerID: ProviderID.zod, - modelID: ModelID.zod, - variant: z.string().optional(), - }), - system: z.string().optional(), - tools: z.record(z.string(), z.boolean()).optional(), - }).meta({ - ref: "UserMessage", }) - export type User = z.infer + .meta({ + ref: "ToolStateError", + }) +export type ToolStateError = z.infer - export const Part = z - .discriminatedUnion("type", [ - TextPart, - SubtaskPart, - ReasoningPart, - FilePart, - ToolPart, - StepStartPart, - StepFinishPart, - SnapshotPart, - PatchPart, - AgentPart, - RetryPart, - CompactionPart, - ]) - .meta({ - ref: "Part", +export const ToolState = z + .discriminatedUnion("status", [ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]) + .meta({ + ref: "ToolState", + }) + +export const ToolPart = PartBase.extend({ + type: z.literal("tool"), + callID: z.string(), + tool: z.string(), + state: ToolState, + metadata: z.record(z.string(), z.any()).optional(), +}).meta({ + ref: "ToolPart", +}) +export type ToolPart = z.infer + +const Base = z.object({ + id: MessageID.zod, + sessionID: SessionID.zod, +}) + +export const User = Base.extend({ + role: z.literal("user"), + time: z.object({ + created: z.number(), + }), + format: Format.optional(), + summary: z + .object({ + title: z.string().optional(), + body: z.string().optional(), + diffs: Snapshot.FileDiff.array(), }) - export type Part = z.infer - - export const Assistant = Base.extend({ - role: z.literal("assistant"), - time: z.object({ - created: z.number(), - completed: z.number().optional(), - }), - error: z - .discriminatedUnion("name", [ - AuthError.Schema, - NamedError.Unknown.Schema, - OutputLengthError.Schema, - AbortedError.Schema, - StructuredOutputError.Schema, - ContextOverflowError.Schema, - APIError.Schema, - ]) - .optional(), - parentID: MessageID.zod, - modelID: ModelID.zod, + .optional(), + agent: z.string(), + model: z.object({ providerID: ProviderID.zod, - /** - * @deprecated - */ - mode: z.string(), - agent: z.string(), - path: z.object({ - cwd: z.string(), - root: z.string(), - }), - summary: z.boolean().optional(), - cost: z.number(), - tokens: z.object({ - total: z.number().optional(), - input: z.number(), - output: z.number(), - reasoning: z.number(), - cache: z.object({ - read: z.number(), - write: z.number(), - }), - }), - structured: z.any().optional(), + modelID: ModelID.zod, variant: z.string().optional(), - finish: z.string().optional(), - }).meta({ - ref: "AssistantMessage", + }), + system: z.string().optional(), + tools: z.record(z.string(), z.boolean()).optional(), +}).meta({ + ref: "UserMessage", +}) +export type User = z.infer + +export const Part = z + .discriminatedUnion("type", [ + TextPart, + SubtaskPart, + ReasoningPart, + FilePart, + ToolPart, + StepStartPart, + StepFinishPart, + SnapshotPart, + PatchPart, + AgentPart, + RetryPart, + CompactionPart, + ]) + .meta({ + ref: "Part", }) - export type Assistant = z.infer +export type Part = z.infer - export const Info = z.discriminatedUnion("role", [User, Assistant]).meta({ - ref: "Message", - }) - export type Info = z.infer - - export const Event = { - Updated: SyncEvent.define({ - type: "message.updated", - version: 1, - aggregate: "sessionID", - schema: z.object({ - sessionID: SessionID.zod, - info: Info, - }), +export const Assistant = Base.extend({ + role: z.literal("assistant"), + time: z.object({ + created: z.number(), + completed: z.number().optional(), + }), + error: z + .discriminatedUnion("name", [ + AuthError.Schema, + NamedError.Unknown.Schema, + OutputLengthError.Schema, + AbortedError.Schema, + StructuredOutputError.Schema, + ContextOverflowError.Schema, + APIError.Schema, + ]) + .optional(), + parentID: MessageID.zod, + modelID: ModelID.zod, + providerID: ProviderID.zod, + /** + * @deprecated + */ + mode: z.string(), + agent: z.string(), + path: z.object({ + cwd: z.string(), + root: z.string(), + }), + summary: z.boolean().optional(), + cost: z.number(), + tokens: z.object({ + total: z.number().optional(), + input: z.number(), + output: z.number(), + reasoning: z.number(), + cache: z.object({ + read: z.number(), + write: z.number(), }), - Removed: SyncEvent.define({ - type: "message.removed", - version: 1, - aggregate: "sessionID", - schema: z.object({ - sessionID: SessionID.zod, - messageID: MessageID.zod, - }), + }), + structured: z.any().optional(), + variant: z.string().optional(), + finish: z.string().optional(), +}).meta({ + ref: "AssistantMessage", +}) +export type Assistant = z.infer + +export const Info = z.discriminatedUnion("role", [User, Assistant]).meta({ + ref: "Message", +}) +export type Info = z.infer + +export const Event = { + Updated: SyncEvent.define({ + type: "message.updated", + version: 1, + aggregate: "sessionID", + schema: z.object({ + sessionID: SessionID.zod, + info: Info, }), - PartUpdated: SyncEvent.define({ - type: "message.part.updated", - version: 1, - aggregate: "sessionID", - schema: z.object({ - sessionID: SessionID.zod, - part: Part, - time: z.number(), - }), + }), + Removed: SyncEvent.define({ + type: "message.removed", + version: 1, + aggregate: "sessionID", + schema: z.object({ + sessionID: SessionID.zod, + messageID: MessageID.zod, }), - PartDelta: BusEvent.define( - "message.part.delta", - z.object({ - sessionID: SessionID.zod, - messageID: MessageID.zod, - partID: PartID.zod, - field: z.string(), - delta: z.string(), - }), - ), - PartRemoved: SyncEvent.define({ - type: "message.part.removed", - version: 1, - aggregate: "sessionID", - schema: z.object({ - sessionID: SessionID.zod, - messageID: MessageID.zod, - partID: PartID.zod, - }), + }), + PartUpdated: SyncEvent.define({ + type: "message.part.updated", + version: 1, + aggregate: "sessionID", + schema: z.object({ + sessionID: SessionID.zod, + part: Part, + time: z.number(), }), - } + }), + PartDelta: BusEvent.define( + "message.part.delta", + z.object({ + sessionID: SessionID.zod, + messageID: MessageID.zod, + partID: PartID.zod, + field: z.string(), + delta: z.string(), + }), + ), + PartRemoved: SyncEvent.define({ + type: "message.part.removed", + version: 1, + aggregate: "sessionID", + schema: z.object({ + sessionID: SessionID.zod, + messageID: MessageID.zod, + partID: PartID.zod, + }), + }), +} - export const WithParts = z.object({ - info: Info, - parts: z.array(Part), - }) - export type WithParts = z.infer +export const WithParts = z.object({ + info: Info, + parts: z.array(Part), +}) +export type WithParts = z.infer - const Cursor = z.object({ - id: MessageID.zod, - time: z.number(), - }) - type Cursor = z.infer +const Cursor = z.object({ + id: MessageID.zod, + time: z.number(), +}) +type Cursor = z.infer - export const cursor = { - encode(input: Cursor) { - return Buffer.from(JSON.stringify(input)).toString("base64url") - }, - decode(input: string) { - return Cursor.parse(JSON.parse(Buffer.from(input, "base64url").toString("utf8"))) - }, - } +export const cursor = { + encode(input: Cursor) { + return Buffer.from(JSON.stringify(input)).toString("base64url") + }, + decode(input: string) { + return Cursor.parse(JSON.parse(Buffer.from(input, "base64url").toString("utf8"))) + }, +} - const info = (row: typeof MessageTable.$inferSelect) => - ({ - ...row.data, - id: row.id, - sessionID: row.session_id, - }) as MessageV2.Info +const info = (row: typeof MessageTable.$inferSelect) => + ({ + ...row.data, + id: row.id, + sessionID: row.session_id, + }) as Info - const part = (row: typeof PartTable.$inferSelect) => - ({ - ...row.data, - id: row.id, - sessionID: row.session_id, - messageID: row.message_id, - }) as MessageV2.Part +const part = (row: typeof PartTable.$inferSelect) => + ({ + ...row.data, + id: row.id, + sessionID: row.session_id, + messageID: row.message_id, + }) as Part - const older = (row: Cursor) => - or( - lt(MessageTable.time_created, row.time), - and(eq(MessageTable.time_created, row.time), lt(MessageTable.id, row.id)), +const older = (row: Cursor) => + or( + lt(MessageTable.time_created, row.time), + and(eq(MessageTable.time_created, row.time), lt(MessageTable.id, row.id)), + ) + +function hydrate(rows: (typeof MessageTable.$inferSelect)[]) { + const ids = rows.map((row) => row.id) + const partByMessage = new Map() + if (ids.length > 0) { + const partRows = Database.use((db) => + db + .select() + .from(PartTable) + .where(inArray(PartTable.message_id, ids)) + .orderBy(PartTable.message_id, PartTable.id) + .all(), ) + for (const row of partRows) { + const next = part(row) + const list = partByMessage.get(row.message_id) + if (list) list.push(next) + else partByMessage.set(row.message_id, [next]) + } + } - function hydrate(rows: (typeof MessageTable.$inferSelect)[]) { - const ids = rows.map((row) => row.id) - const partByMessage = new Map() - if (ids.length > 0) { - const partRows = Database.use((db) => - db - .select() - .from(PartTable) - .where(inArray(PartTable.message_id, ids)) - .orderBy(PartTable.message_id, PartTable.id) - .all(), - ) - for (const row of partRows) { - const next = part(row) - const list = partByMessage.get(row.message_id) - if (list) list.push(next) - else partByMessage.set(row.message_id, [next]) + return rows.map((row) => ({ + info: info(row), + parts: partByMessage.get(row.id) ?? [], + })) +} + +function providerMeta(metadata: Record | undefined) { + if (!metadata) return undefined + const { providerExecuted: _, ...rest } = metadata + return Object.keys(rest).length > 0 ? rest : undefined +} + +export const toModelMessagesEffect = Effect.fnUntraced(function* ( + input: WithParts[], + model: Provider.Model, + options?: { stripMedia?: boolean }, +) { + const result: UIMessage[] = [] + const toolNames = new Set() + // Track media from tool results that need to be injected as user messages + // for providers that don't support media in tool results. + // + // OpenAI-compatible APIs only support string content in tool results, so we need + // to extract media and inject as user messages. Other SDKs (anthropic, google, + // bedrock) handle type: "content" with media parts natively. + // + // Only apply this workaround if the model actually supports image input - + // otherwise there's no point extracting images. + const supportsMediaInToolResults = (() => { + if (model.api.npm === "@ai-sdk/anthropic") return true + if (model.api.npm === "@ai-sdk/openai") return true + if (model.api.npm === "@ai-sdk/amazon-bedrock") return true + if (model.api.npm === "@ai-sdk/google-vertex/anthropic") return true + if (model.api.npm === "@ai-sdk/google") { + const id = model.api.id.toLowerCase() + return id.includes("gemini-3") && !id.includes("gemini-2") + } + return false + })() + + const toModelOutput = (options: { toolCallId: string; input: unknown; output: unknown }) => { + const output = options.output + if (typeof output === "string") { + return { type: "text", value: output } + } + + if (typeof output === "object") { + const outputObject = output as { + text: string + attachments?: Array<{ mime: string; url: string }> + } + const attachments = (outputObject.attachments ?? []).filter((attachment) => { + return attachment.url.startsWith("data:") && attachment.url.includes(",") + }) + + return { + type: "content", + value: [ + { type: "text", text: outputObject.text }, + ...attachments.map((attachment) => ({ + type: "media", + mediaType: attachment.mime, + data: iife(() => { + const commaIndex = attachment.url.indexOf(",") + return commaIndex === -1 ? attachment.url : attachment.url.slice(commaIndex + 1) + }), + })), + ], } } - return rows.map((row) => ({ - info: info(row), - parts: partByMessage.get(row.id) ?? [], - })) + return { type: "json", value: output as never } } - function providerMeta(metadata: Record | undefined) { - if (!metadata) return undefined - const { providerExecuted: _, ...rest } = metadata - return Object.keys(rest).length > 0 ? rest : undefined - } + for (const msg of input) { + if (msg.parts.length === 0) continue - export const toModelMessagesEffect = Effect.fnUntraced(function* ( - input: WithParts[], - model: Provider.Model, - options?: { stripMedia?: boolean }, - ) { - const result: UIMessage[] = [] - const toolNames = new Set() - // Track media from tool results that need to be injected as user messages - // for providers that don't support media in tool results. - // - // OpenAI-compatible APIs only support string content in tool results, so we need - // to extract media and inject as user messages. Other SDKs (anthropic, google, - // bedrock) handle type: "content" with media parts natively. - // - // Only apply this workaround if the model actually supports image input - - // otherwise there's no point extracting images. - const supportsMediaInToolResults = (() => { - if (model.api.npm === "@ai-sdk/anthropic") return true - if (model.api.npm === "@ai-sdk/openai") return true - if (model.api.npm === "@ai-sdk/amazon-bedrock") return true - if (model.api.npm === "@ai-sdk/google-vertex/anthropic") return true - if (model.api.npm === "@ai-sdk/google") { - const id = model.api.id.toLowerCase() - return id.includes("gemini-3") && !id.includes("gemini-2") + if (msg.info.role === "user") { + const userMessage: UIMessage = { + id: msg.info.id, + role: "user", + parts: [], } - return false - })() - - const toModelOutput = (options: { toolCallId: string; input: unknown; output: unknown }) => { - const output = options.output - if (typeof output === "string") { - return { type: "text", value: output } - } - - if (typeof output === "object") { - const outputObject = output as { - text: string - attachments?: Array<{ mime: string; url: string }> - } - const attachments = (outputObject.attachments ?? []).filter((attachment) => { - return attachment.url.startsWith("data:") && attachment.url.includes(",") - }) - - return { - type: "content", - value: [ - { type: "text", text: outputObject.text }, - ...attachments.map((attachment) => ({ - type: "media", - mediaType: attachment.mime, - data: iife(() => { - const commaIndex = attachment.url.indexOf(",") - return commaIndex === -1 ? attachment.url : attachment.url.slice(commaIndex + 1) - }), - })), - ], - } - } - - return { type: "json", value: output as never } - } - - for (const msg of input) { - if (msg.parts.length === 0) continue - - if (msg.info.role === "user") { - const userMessage: UIMessage = { - id: msg.info.id, - role: "user", - parts: [], - } - result.push(userMessage) - for (const part of msg.parts) { - if (part.type === "text" && !part.ignored) + result.push(userMessage) + for (const part of msg.parts) { + if (part.type === "text" && !part.ignored) + userMessage.parts.push({ + type: "text", + text: part.text, + }) + // text/plain and directory files are converted into text parts, ignore them + if (part.type === "file" && part.mime !== "text/plain" && part.mime !== "application/x-directory") { + if (options?.stripMedia && isMedia(part.mime)) { userMessage.parts.push({ type: "text", - text: part.text, + text: `[Attached ${part.mime}: ${part.filename ?? "file"}]`, }) - // text/plain and directory files are converted into text parts, ignore them - if (part.type === "file" && part.mime !== "text/plain" && part.mime !== "application/x-directory") { - if (options?.stripMedia && isMedia(part.mime)) { - userMessage.parts.push({ - type: "text", - text: `[Attached ${part.mime}: ${part.filename ?? "file"}]`, - }) - } else { - userMessage.parts.push({ - type: "file", - url: part.url, - mediaType: part.mime, - filename: part.filename, - }) + } else { + userMessage.parts.push({ + type: "file", + url: part.url, + mediaType: part.mime, + filename: part.filename, + }) + } + } + + if (part.type === "compaction") { + userMessage.parts.push({ + type: "text", + text: "What did we do so far?", + }) + } + if (part.type === "subtask") { + userMessage.parts.push({ + type: "text", + text: "The following tool was executed by the user", + }) + } + } + } + + if (msg.info.role === "assistant") { + const differentModel = `${model.providerID}/${model.id}` !== `${msg.info.providerID}/${msg.info.modelID}` + const media: Array<{ mime: string; url: string }> = [] + + if ( + msg.info.error && + !( + AbortedError.isInstance(msg.info.error) && + msg.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning") + ) + ) { + continue + } + const assistantMessage: UIMessage = { + id: msg.info.id, + role: "assistant", + parts: [], + } + for (const part of msg.parts) { + if (part.type === "text") + assistantMessage.parts.push({ + type: "text", + text: part.text, + ...(differentModel ? {} : { providerMetadata: part.metadata }), + }) + if (part.type === "step-start") + assistantMessage.parts.push({ + type: "step-start", + }) + if (part.type === "tool") { + toolNames.add(part.tool) + if (part.state.status === "completed") { + const outputText = part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output + const attachments = part.state.time.compacted || options?.stripMedia ? [] : (part.state.attachments ?? []) + + // For providers that don't support media in tool results, extract media files + // (images, PDFs) to be sent as a separate user message + const mediaAttachments = attachments.filter((a) => isMedia(a.mime)) + const nonMediaAttachments = attachments.filter((a) => !isMedia(a.mime)) + if (!supportsMediaInToolResults && mediaAttachments.length > 0) { + media.push(...mediaAttachments) } - } + const finalAttachments = supportsMediaInToolResults ? attachments : nonMediaAttachments - if (part.type === "compaction") { - userMessage.parts.push({ - type: "text", - text: "What did we do so far?", - }) - } - if (part.type === "subtask") { - userMessage.parts.push({ - type: "text", - text: "The following tool was executed by the user", - }) - } - } - } + const output = + finalAttachments.length > 0 + ? { + text: outputText, + attachments: finalAttachments, + } + : outputText - if (msg.info.role === "assistant") { - const differentModel = `${model.providerID}/${model.id}` !== `${msg.info.providerID}/${msg.info.modelID}` - const media: Array<{ mime: string; url: string }> = [] - - if ( - msg.info.error && - !( - MessageV2.AbortedError.isInstance(msg.info.error) && - msg.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning") - ) - ) { - continue - } - const assistantMessage: UIMessage = { - id: msg.info.id, - role: "assistant", - parts: [], - } - for (const part of msg.parts) { - if (part.type === "text") assistantMessage.parts.push({ - type: "text", - text: part.text, - ...(differentModel ? {} : { providerMetadata: part.metadata }), + type: ("tool-" + part.tool) as `tool-${string}`, + state: "output-available", + toolCallId: part.callID, + input: part.state.input, + output, + ...(part.metadata?.providerExecuted ? { providerExecuted: true } : {}), + ...(differentModel ? {} : { callProviderMetadata: providerMeta(part.metadata) }), }) - if (part.type === "step-start") - assistantMessage.parts.push({ - type: "step-start", - }) - if (part.type === "tool") { - toolNames.add(part.tool) - if (part.state.status === "completed") { - const outputText = part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output - const attachments = part.state.time.compacted || options?.stripMedia ? [] : (part.state.attachments ?? []) - - // For providers that don't support media in tool results, extract media files - // (images, PDFs) to be sent as a separate user message - const mediaAttachments = attachments.filter((a) => isMedia(a.mime)) - const nonMediaAttachments = attachments.filter((a) => !isMedia(a.mime)) - if (!supportsMediaInToolResults && mediaAttachments.length > 0) { - media.push(...mediaAttachments) - } - const finalAttachments = supportsMediaInToolResults ? attachments : nonMediaAttachments - - const output = - finalAttachments.length > 0 - ? { - text: outputText, - attachments: finalAttachments, - } - : outputText - + } + if (part.state.status === "error") { + const output = part.state.metadata?.interrupted === true ? part.state.metadata.output : undefined + if (typeof output === "string") { assistantMessage.parts.push({ type: ("tool-" + part.tool) as `tool-${string}`, state: "output-available", @@ -753,305 +765,293 @@ export namespace MessageV2 { ...(part.metadata?.providerExecuted ? { providerExecuted: true } : {}), ...(differentModel ? {} : { callProviderMetadata: providerMeta(part.metadata) }), }) - } - if (part.state.status === "error") { - const output = part.state.metadata?.interrupted === true ? part.state.metadata.output : undefined - if (typeof output === "string") { - assistantMessage.parts.push({ - type: ("tool-" + part.tool) as `tool-${string}`, - state: "output-available", - toolCallId: part.callID, - input: part.state.input, - output, - ...(part.metadata?.providerExecuted ? { providerExecuted: true } : {}), - ...(differentModel ? {} : { callProviderMetadata: providerMeta(part.metadata) }), - }) - } else { - assistantMessage.parts.push({ - type: ("tool-" + part.tool) as `tool-${string}`, - state: "output-error", - toolCallId: part.callID, - input: part.state.input, - errorText: part.state.error, - ...(part.metadata?.providerExecuted ? { providerExecuted: true } : {}), - ...(differentModel ? {} : { callProviderMetadata: providerMeta(part.metadata) }), - }) - } - } - // Handle pending/running tool calls to prevent dangling tool_use blocks - // Anthropic/Claude APIs require every tool_use to have a corresponding tool_result - if (part.state.status === "pending" || part.state.status === "running") + } else { assistantMessage.parts.push({ type: ("tool-" + part.tool) as `tool-${string}`, state: "output-error", toolCallId: part.callID, input: part.state.input, - errorText: "[Tool execution was interrupted]", + errorText: part.state.error, ...(part.metadata?.providerExecuted ? { providerExecuted: true } : {}), ...(differentModel ? {} : { callProviderMetadata: providerMeta(part.metadata) }), }) - } - if (part.type === "reasoning") { - assistantMessage.parts.push({ - type: "reasoning", - text: part.text, - ...(differentModel ? {} : { providerMetadata: part.metadata }), - }) - } - } - if (assistantMessage.parts.length > 0) { - result.push(assistantMessage) - // Inject pending media as a user message for providers that don't support - // media (images, PDFs) in tool results - if (media.length > 0) { - result.push({ - id: MessageID.ascending(), - role: "user", - parts: [ - { - type: "text" as const, - text: SYNTHETIC_ATTACHMENT_PROMPT, - }, - ...media.map((attachment) => ({ - type: "file" as const, - url: attachment.url, - mediaType: attachment.mime, - })), - ], - }) - } - } - } - } - - const tools = Object.fromEntries(Array.from(toolNames).map((toolName) => [toolName, { toModelOutput }])) - - return yield* Effect.promise(() => - convertToModelMessages( - result.filter((msg) => msg.parts.some((part) => part.type !== "step-start")), - { - //@ts-expect-error (convertToModelMessages expects a ToolSet but only actually needs tools[name]?.toModelOutput) - tools, - }, - ), - ) - }) - - export function toModelMessages( - input: WithParts[], - model: Provider.Model, - options?: { stripMedia?: boolean }, - ): Promise { - return Effect.runPromise(toModelMessagesEffect(input, model, options).pipe(Effect.provide(EffectLogger.layer))) - } - - export function page(input: { sessionID: SessionID; limit: number; before?: string }) { - const before = input.before ? cursor.decode(input.before) : undefined - const where = before - ? and(eq(MessageTable.session_id, input.sessionID), older(before)) - : eq(MessageTable.session_id, input.sessionID) - const rows = Database.use((db) => - db - .select() - .from(MessageTable) - .where(where) - .orderBy(desc(MessageTable.time_created), desc(MessageTable.id)) - .limit(input.limit + 1) - .all(), - ) - if (rows.length === 0) { - const row = Database.use((db) => - db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.id, input.sessionID)).get(), - ) - if (!row) throw new NotFoundError({ message: `Session not found: ${input.sessionID}` }) - return { - items: [] as MessageV2.WithParts[], - more: false, - } - } - - const more = rows.length > input.limit - const slice = more ? rows.slice(0, input.limit) : rows - const items = hydrate(slice) - items.reverse() - const tail = slice.at(-1) - return { - items, - more, - cursor: more && tail ? cursor.encode({ id: tail.id, time: tail.time_created }) : undefined, - } - } - - export function* stream(sessionID: SessionID) { - const size = 50 - let before: string | undefined - while (true) { - const next = page({ sessionID, limit: size, before }) - if (next.items.length === 0) break - for (let i = next.items.length - 1; i >= 0; i--) { - yield next.items[i] - } - if (!next.more || !next.cursor) break - before = next.cursor - } - } - - export function parts(message_id: MessageID) { - const rows = Database.use((db) => - db.select().from(PartTable).where(eq(PartTable.message_id, message_id)).orderBy(PartTable.id).all(), - ) - return rows.map( - (row) => - ({ - ...row.data, - id: row.id, - sessionID: row.session_id, - messageID: row.message_id, - }) as MessageV2.Part, - ) - } - - export function get(input: { sessionID: SessionID; messageID: MessageID }): WithParts { - const row = Database.use((db) => - db - .select() - .from(MessageTable) - .where(and(eq(MessageTable.id, input.messageID), eq(MessageTable.session_id, input.sessionID))) - .get(), - ) - if (!row) throw new NotFoundError({ message: `Message not found: ${input.messageID}` }) - return { - info: info(row), - parts: parts(input.messageID), - } - } - - export function filterCompacted(msgs: Iterable) { - const result = [] as MessageV2.WithParts[] - const completed = new Set() - for (const msg of msgs) { - result.push(msg) - if ( - msg.info.role === "user" && - completed.has(msg.info.id) && - msg.parts.some((part) => part.type === "compaction") - ) - break - if (msg.info.role === "assistant" && msg.info.summary && msg.info.finish && !msg.info.error) - completed.add(msg.info.parentID) - } - result.reverse() - return result - } - - export const filterCompactedEffect = Effect.fnUntraced(function* (sessionID: SessionID) { - return filterCompacted(stream(sessionID)) - }) - - export function fromError( - e: unknown, - ctx: { providerID: ProviderID; aborted?: boolean }, - ): NonNullable { - switch (true) { - case e instanceof DOMException && e.name === "AbortError": - return new MessageV2.AbortedError( - { message: e.message }, - { - cause: e, - }, - ).toObject() - case MessageV2.OutputLengthError.isInstance(e): - return e - case LoadAPIKeyError.isInstance(e): - return new MessageV2.AuthError( - { - providerID: ctx.providerID, - message: e.message, - }, - { cause: e }, - ).toObject() - case (e as SystemError)?.code === "ECONNRESET": - return new MessageV2.APIError( - { - message: "Connection reset by server", - isRetryable: true, - metadata: { - code: (e as SystemError).code ?? "", - syscall: (e as SystemError).syscall ?? "", - message: (e as SystemError).message ?? "", - }, - }, - { cause: e }, - ).toObject() - case e instanceof Error && (e as FetchDecompressionError).code === "ZlibError": - if (ctx.aborted) { - return new MessageV2.AbortedError({ message: e.message }, { cause: e }).toObject() - } - return new MessageV2.APIError( - { - message: "Response decompression failed", - isRetryable: true, - metadata: { - code: (e as FetchDecompressionError).code, - message: e.message, - }, - }, - { cause: e }, - ).toObject() - case APICallError.isInstance(e): - const parsed = ProviderError.parseAPICallError({ - providerID: ctx.providerID, - error: e, - }) - if (parsed.type === "context_overflow") { - return new MessageV2.ContextOverflowError( - { - message: parsed.message, - responseBody: parsed.responseBody, - }, - { cause: e }, - ).toObject() - } - - return new MessageV2.APIError( - { - message: parsed.message, - statusCode: parsed.statusCode, - isRetryable: parsed.isRetryable, - responseHeaders: parsed.responseHeaders, - responseBody: parsed.responseBody, - metadata: parsed.metadata, - }, - { cause: e }, - ).toObject() - case e instanceof Error: - return new NamedError.Unknown({ message: errorMessage(e) }, { cause: e }).toObject() - default: - try { - const parsed = ProviderError.parseStreamError(e) - if (parsed) { - if (parsed.type === "context_overflow") { - return new MessageV2.ContextOverflowError( - { - message: parsed.message, - responseBody: parsed.responseBody, - }, - { cause: e }, - ).toObject() } - return new MessageV2.APIError( - { - message: parsed.message, - isRetryable: parsed.isRetryable, - responseBody: parsed.responseBody, - }, - { - cause: e, - }, - ).toObject() } - } catch {} - return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject() + // Handle pending/running tool calls to prevent dangling tool_use blocks + // Anthropic/Claude APIs require every tool_use to have a corresponding tool_result + if (part.state.status === "pending" || part.state.status === "running") + assistantMessage.parts.push({ + type: ("tool-" + part.tool) as `tool-${string}`, + state: "output-error", + toolCallId: part.callID, + input: part.state.input, + errorText: "[Tool execution was interrupted]", + ...(part.metadata?.providerExecuted ? { providerExecuted: true } : {}), + ...(differentModel ? {} : { callProviderMetadata: providerMeta(part.metadata) }), + }) + } + if (part.type === "reasoning") { + assistantMessage.parts.push({ + type: "reasoning", + text: part.text, + ...(differentModel ? {} : { providerMetadata: part.metadata }), + }) + } + } + if (assistantMessage.parts.length > 0) { + result.push(assistantMessage) + // Inject pending media as a user message for providers that don't support + // media (images, PDFs) in tool results + if (media.length > 0) { + result.push({ + id: MessageID.ascending(), + role: "user", + parts: [ + { + type: "text" as const, + text: SYNTHETIC_ATTACHMENT_PROMPT, + }, + ...media.map((attachment) => ({ + type: "file" as const, + url: attachment.url, + mediaType: attachment.mime, + })), + ], + }) + } + } } } + + const tools = Object.fromEntries(Array.from(toolNames).map((toolName) => [toolName, { toModelOutput }])) + + return yield* Effect.promise(() => + convertToModelMessages( + result.filter((msg) => msg.parts.some((part) => part.type !== "step-start")), + { + //@ts-expect-error (convertToModelMessages expects a ToolSet but only actually needs tools[name]?.toModelOutput) + tools, + }, + ), + ) +}) + +export function toModelMessages( + input: WithParts[], + model: Provider.Model, + options?: { stripMedia?: boolean }, +): Promise { + return Effect.runPromise(toModelMessagesEffect(input, model, options).pipe(Effect.provide(EffectLogger.layer))) +} + +export function page(input: { sessionID: SessionID; limit: number; before?: string }) { + const before = input.before ? cursor.decode(input.before) : undefined + const where = before + ? and(eq(MessageTable.session_id, input.sessionID), older(before)) + : eq(MessageTable.session_id, input.sessionID) + const rows = Database.use((db) => + db + .select() + .from(MessageTable) + .where(where) + .orderBy(desc(MessageTable.time_created), desc(MessageTable.id)) + .limit(input.limit + 1) + .all(), + ) + if (rows.length === 0) { + const row = Database.use((db) => + db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.id, input.sessionID)).get(), + ) + if (!row) throw new NotFoundError({ message: `Session not found: ${input.sessionID}` }) + return { + items: [] as WithParts[], + more: false, + } + } + + const more = rows.length > input.limit + const slice = more ? rows.slice(0, input.limit) : rows + const items = hydrate(slice) + items.reverse() + const tail = slice.at(-1) + return { + items, + more, + cursor: more && tail ? cursor.encode({ id: tail.id, time: tail.time_created }) : undefined, + } } + +export function* stream(sessionID: SessionID) { + const size = 50 + let before: string | undefined + while (true) { + const next = page({ sessionID, limit: size, before }) + if (next.items.length === 0) break + for (let i = next.items.length - 1; i >= 0; i--) { + yield next.items[i] + } + if (!next.more || !next.cursor) break + before = next.cursor + } +} + +export function parts(message_id: MessageID) { + const rows = Database.use((db) => + db.select().from(PartTable).where(eq(PartTable.message_id, message_id)).orderBy(PartTable.id).all(), + ) + return rows.map( + (row) => + ({ + ...row.data, + id: row.id, + sessionID: row.session_id, + messageID: row.message_id, + }) as Part, + ) +} + +export function get(input: { sessionID: SessionID; messageID: MessageID }): WithParts { + const row = Database.use((db) => + db + .select() + .from(MessageTable) + .where(and(eq(MessageTable.id, input.messageID), eq(MessageTable.session_id, input.sessionID))) + .get(), + ) + if (!row) throw new NotFoundError({ message: `Message not found: ${input.messageID}` }) + return { + info: info(row), + parts: parts(input.messageID), + } +} + +export function filterCompacted(msgs: Iterable) { + const result = [] as WithParts[] + const completed = new Set() + for (const msg of msgs) { + result.push(msg) + if ( + msg.info.role === "user" && + completed.has(msg.info.id) && + msg.parts.some((part) => part.type === "compaction") + ) + break + if (msg.info.role === "assistant" && msg.info.summary && msg.info.finish && !msg.info.error) + completed.add(msg.info.parentID) + } + result.reverse() + return result +} + +export const filterCompactedEffect = Effect.fnUntraced(function* (sessionID: SessionID) { + return filterCompacted(stream(sessionID)) +}) + +export function fromError( + e: unknown, + ctx: { providerID: ProviderID; aborted?: boolean }, +): NonNullable { + switch (true) { + case e instanceof DOMException && e.name === "AbortError": + return new AbortedError( + { message: e.message }, + { + cause: e, + }, + ).toObject() + case OutputLengthError.isInstance(e): + return e + case LoadAPIKeyError.isInstance(e): + return new AuthError( + { + providerID: ctx.providerID, + message: e.message, + }, + { cause: e }, + ).toObject() + case (e as SystemError)?.code === "ECONNRESET": + return new APIError( + { + message: "Connection reset by server", + isRetryable: true, + metadata: { + code: (e as SystemError).code ?? "", + syscall: (e as SystemError).syscall ?? "", + message: (e as SystemError).message ?? "", + }, + }, + { cause: e }, + ).toObject() + case e instanceof Error && (e as FetchDecompressionError).code === "ZlibError": + if (ctx.aborted) { + return new AbortedError({ message: e.message }, { cause: e }).toObject() + } + return new APIError( + { + message: "Response decompression failed", + isRetryable: true, + metadata: { + code: (e as FetchDecompressionError).code, + message: e.message, + }, + }, + { cause: e }, + ).toObject() + case APICallError.isInstance(e): + const parsed = ProviderError.parseAPICallError({ + providerID: ctx.providerID, + error: e, + }) + if (parsed.type === "context_overflow") { + return new ContextOverflowError( + { + message: parsed.message, + responseBody: parsed.responseBody, + }, + { cause: e }, + ).toObject() + } + + return new APIError( + { + message: parsed.message, + statusCode: parsed.statusCode, + isRetryable: parsed.isRetryable, + responseHeaders: parsed.responseHeaders, + responseBody: parsed.responseBody, + metadata: parsed.metadata, + }, + { cause: e }, + ).toObject() + case e instanceof Error: + return new NamedError.Unknown({ message: errorMessage(e) }, { cause: e }).toObject() + default: + try { + const parsed = ProviderError.parseStreamError(e) + if (parsed) { + if (parsed.type === "context_overflow") { + return new ContextOverflowError( + { + message: parsed.message, + responseBody: parsed.responseBody, + }, + { cause: e }, + ).toObject() + } + return new APIError( + { + message: parsed.message, + isRetryable: parsed.isRetryable, + responseBody: parsed.responseBody, + }, + { + cause: e, + }, + ).toObject() + } + } catch {} + return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject() + } +} + +export * as MessageV2 from "./message-v2" diff --git a/packages/opencode/src/session/message.ts b/packages/opencode/src/session/message.ts index 396034825a..ced04b8e9d 100644 --- a/packages/opencode/src/session/message.ts +++ b/packages/opencode/src/session/message.ts @@ -3,189 +3,189 @@ import { SessionID } from "./schema" import { ModelID, ProviderID } from "../provider/schema" import { NamedError } from "@opencode-ai/shared/util/error" -export namespace Message { - export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({})) - export const AuthError = NamedError.create( - "ProviderAuthError", - z.object({ - providerID: z.string(), - message: z.string(), - }), - ) +export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({})) +export const AuthError = NamedError.create( + "ProviderAuthError", + z.object({ + providerID: z.string(), + message: z.string(), + }), +) - export const ToolCall = z - .object({ - state: z.literal("call"), - step: z.number().optional(), - toolCallId: z.string(), - toolName: z.string(), - args: z.custom>(), - }) - .meta({ - ref: "ToolCall", - }) - export type ToolCall = z.infer - - export const ToolPartialCall = z - .object({ - state: z.literal("partial-call"), - step: z.number().optional(), - toolCallId: z.string(), - toolName: z.string(), - args: z.custom>(), - }) - .meta({ - ref: "ToolPartialCall", - }) - export type ToolPartialCall = z.infer - - export const ToolResult = z - .object({ - state: z.literal("result"), - step: z.number().optional(), - toolCallId: z.string(), - toolName: z.string(), - args: z.custom>(), - result: z.string(), - }) - .meta({ - ref: "ToolResult", - }) - export type ToolResult = z.infer - - export const ToolInvocation = z.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]).meta({ - ref: "ToolInvocation", +export const ToolCall = z + .object({ + state: z.literal("call"), + step: z.number().optional(), + toolCallId: z.string(), + toolName: z.string(), + args: z.custom>(), }) - export type ToolInvocation = z.infer + .meta({ + ref: "ToolCall", + }) +export type ToolCall = z.infer - export const TextPart = z - .object({ - type: z.literal("text"), - text: z.string(), - }) - .meta({ - ref: "TextPart", - }) - export type TextPart = z.infer +export const ToolPartialCall = z + .object({ + state: z.literal("partial-call"), + step: z.number().optional(), + toolCallId: z.string(), + toolName: z.string(), + args: z.custom>(), + }) + .meta({ + ref: "ToolPartialCall", + }) +export type ToolPartialCall = z.infer - export const ReasoningPart = z - .object({ - type: z.literal("reasoning"), - text: z.string(), - providerMetadata: z.record(z.string(), z.any()).optional(), - }) - .meta({ - ref: "ReasoningPart", - }) - export type ReasoningPart = z.infer +export const ToolResult = z + .object({ + state: z.literal("result"), + step: z.number().optional(), + toolCallId: z.string(), + toolName: z.string(), + args: z.custom>(), + result: z.string(), + }) + .meta({ + ref: "ToolResult", + }) +export type ToolResult = z.infer - export const ToolInvocationPart = z - .object({ - type: z.literal("tool-invocation"), - toolInvocation: ToolInvocation, - }) - .meta({ - ref: "ToolInvocationPart", - }) - export type ToolInvocationPart = z.infer +export const ToolInvocation = z.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]).meta({ + ref: "ToolInvocation", +}) +export type ToolInvocation = z.infer - export const SourceUrlPart = z - .object({ - type: z.literal("source-url"), - sourceId: z.string(), - url: z.string(), - title: z.string().optional(), - providerMetadata: z.record(z.string(), z.any()).optional(), - }) - .meta({ - ref: "SourceUrlPart", - }) - export type SourceUrlPart = z.infer +export const TextPart = z + .object({ + type: z.literal("text"), + text: z.string(), + }) + .meta({ + ref: "TextPart", + }) +export type TextPart = z.infer - export const FilePart = z - .object({ - type: z.literal("file"), - mediaType: z.string(), - filename: z.string().optional(), - url: z.string(), - }) - .meta({ - ref: "FilePart", - }) - export type FilePart = z.infer +export const ReasoningPart = z + .object({ + type: z.literal("reasoning"), + text: z.string(), + providerMetadata: z.record(z.string(), z.any()).optional(), + }) + .meta({ + ref: "ReasoningPart", + }) +export type ReasoningPart = z.infer - export const StepStartPart = z - .object({ - type: z.literal("step-start"), - }) - .meta({ - ref: "StepStartPart", - }) - export type StepStartPart = z.infer +export const ToolInvocationPart = z + .object({ + type: z.literal("tool-invocation"), + toolInvocation: ToolInvocation, + }) + .meta({ + ref: "ToolInvocationPart", + }) +export type ToolInvocationPart = z.infer - export const MessagePart = z - .discriminatedUnion("type", [TextPart, ReasoningPart, ToolInvocationPart, SourceUrlPart, FilePart, StepStartPart]) - .meta({ - ref: "MessagePart", - }) - export type MessagePart = z.infer +export const SourceUrlPart = z + .object({ + type: z.literal("source-url"), + sourceId: z.string(), + url: z.string(), + title: z.string().optional(), + providerMetadata: z.record(z.string(), z.any()).optional(), + }) + .meta({ + ref: "SourceUrlPart", + }) +export type SourceUrlPart = z.infer - export const Info = z - .object({ - id: z.string(), - role: z.enum(["user", "assistant"]), - parts: z.array(MessagePart), - metadata: z - .object({ - time: z.object({ - created: z.number(), - completed: z.number().optional(), - }), - error: z - .discriminatedUnion("name", [AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema]) - .optional(), - sessionID: SessionID.zod, - tool: z.record( - z.string(), - z - .object({ - title: z.string(), - snapshot: z.string().optional(), - time: z.object({ - start: z.number(), - end: z.number(), - }), - }) - .catchall(z.any()), - ), - assistant: z +export const FilePart = z + .object({ + type: z.literal("file"), + mediaType: z.string(), + filename: z.string().optional(), + url: z.string(), + }) + .meta({ + ref: "FilePart", + }) +export type FilePart = z.infer + +export const StepStartPart = z + .object({ + type: z.literal("step-start"), + }) + .meta({ + ref: "StepStartPart", + }) +export type StepStartPart = z.infer + +export const MessagePart = z + .discriminatedUnion("type", [TextPart, ReasoningPart, ToolInvocationPart, SourceUrlPart, FilePart, StepStartPart]) + .meta({ + ref: "MessagePart", + }) +export type MessagePart = z.infer + +export const Info = z + .object({ + id: z.string(), + role: z.enum(["user", "assistant"]), + parts: z.array(MessagePart), + metadata: z + .object({ + time: z.object({ + created: z.number(), + completed: z.number().optional(), + }), + error: z + .discriminatedUnion("name", [AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema]) + .optional(), + sessionID: SessionID.zod, + tool: z.record( + z.string(), + z .object({ - system: z.string().array(), - modelID: ModelID.zod, - providerID: ProviderID.zod, - path: z.object({ - cwd: z.string(), - root: z.string(), - }), - cost: z.number(), - summary: z.boolean().optional(), - tokens: z.object({ - input: z.number(), - output: z.number(), - reasoning: z.number(), - cache: z.object({ - read: z.number(), - write: z.number(), - }), + title: z.string(), + snapshot: z.string().optional(), + time: z.object({ + start: z.number(), + end: z.number(), }), }) - .optional(), - snapshot: z.string().optional(), - }) - .meta({ ref: "MessageMetadata" }), - }) - .meta({ - ref: "Message", - }) - export type Info = z.infer -} + .catchall(z.any()), + ), + assistant: z + .object({ + system: z.string().array(), + modelID: ModelID.zod, + providerID: ProviderID.zod, + path: z.object({ + cwd: z.string(), + root: z.string(), + }), + cost: z.number(), + summary: z.boolean().optional(), + tokens: z.object({ + input: z.number(), + output: z.number(), + reasoning: z.number(), + cache: z.object({ + read: z.number(), + write: z.number(), + }), + }), + }) + .optional(), + snapshot: z.string().optional(), + }) + .meta({ ref: "MessageMetadata" }), + }) + .meta({ + ref: "Message", + }) +export type Info = z.infer + +export * as Message from "./message" diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 415639fbe5..820c61aa91 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -21,599 +21,599 @@ import { errorMessage } from "@/util/error" import { Log } from "@/util" import { isRecord } from "@/util/record" -export namespace SessionProcessor { - const DOOM_LOOP_THRESHOLD = 3 - const log = Log.create({ service: "session.processor" }) +const DOOM_LOOP_THRESHOLD = 3 +const log = Log.create({ service: "session.processor" }) - export type Result = "compact" | "stop" | "continue" +export type Result = "compact" | "stop" | "continue" - export type Event = LLM.Event +export type Event = LLM.Event - export interface Handle { - readonly message: MessageV2.Assistant - readonly updateToolCall: ( - toolCallID: string, - update: (part: MessageV2.ToolPart) => MessageV2.ToolPart, - ) => Effect.Effect - readonly completeToolCall: ( - toolCallID: string, - output: { - title: string - metadata: Record - output: string - attachments?: MessageV2.FilePart[] - }, - ) => Effect.Effect - readonly process: (streamInput: LLM.StreamInput) => Effect.Effect - } +export interface Handle { + readonly message: MessageV2.Assistant + readonly updateToolCall: ( + toolCallID: string, + update: (part: MessageV2.ToolPart) => MessageV2.ToolPart, + ) => Effect.Effect + readonly completeToolCall: ( + toolCallID: string, + output: { + title: string + metadata: Record + output: string + attachments?: MessageV2.FilePart[] + }, + ) => Effect.Effect + readonly process: (streamInput: LLM.StreamInput) => Effect.Effect +} - type Input = { - assistantMessage: MessageV2.Assistant - sessionID: SessionID - model: Provider.Model - } +type Input = { + assistantMessage: MessageV2.Assistant + sessionID: SessionID + model: Provider.Model +} - export interface Interface { - readonly create: (input: Input) => Effect.Effect - } +export interface Interface { + readonly create: (input: Input) => Effect.Effect +} - type ToolCall = { - partID: MessageV2.ToolPart["id"] - messageID: MessageV2.ToolPart["messageID"] - sessionID: MessageV2.ToolPart["sessionID"] - done: Deferred.Deferred - } +type ToolCall = { + partID: MessageV2.ToolPart["id"] + messageID: MessageV2.ToolPart["messageID"] + sessionID: MessageV2.ToolPart["sessionID"] + done: Deferred.Deferred +} - interface ProcessorContext extends Input { - toolcalls: Record - shouldBreak: boolean - snapshot: string | undefined - blocked: boolean - needsCompaction: boolean - currentText: MessageV2.TextPart | undefined - reasoningMap: Record - } +interface ProcessorContext extends Input { + toolcalls: Record + shouldBreak: boolean + snapshot: string | undefined + blocked: boolean + needsCompaction: boolean + currentText: MessageV2.TextPart | undefined + reasoningMap: Record +} - type StreamEvent = Event +type StreamEvent = Event - export class Service extends Context.Service()("@opencode/SessionProcessor") {} +export class Service extends Context.Service()("@opencode/SessionProcessor") {} - export const layer: Layer.Layer< - Service, - never, - | Session.Service - | Config.Service - | Bus.Service - | Snapshot.Service - | Agent.Service - | LLM.Service - | Permission.Service - | Plugin.Service - | SessionSummary.Service - | SessionStatus.Service - > = Layer.effect( - Service, - Effect.gen(function* () { - const session = yield* Session.Service - const config = yield* Config.Service - const bus = yield* Bus.Service - const snapshot = yield* Snapshot.Service - const agents = yield* Agent.Service - const llm = yield* LLM.Service - const permission = yield* Permission.Service - const plugin = yield* Plugin.Service - const summary = yield* SessionSummary.Service - const scope = yield* Scope.Scope - const status = yield* SessionStatus.Service +export const layer: Layer.Layer< + Service, + never, + | Session.Service + | Config.Service + | Bus.Service + | Snapshot.Service + | Agent.Service + | LLM.Service + | Permission.Service + | Plugin.Service + | SessionSummary.Service + | SessionStatus.Service +> = Layer.effect( + Service, + Effect.gen(function* () { + const session = yield* Session.Service + const config = yield* Config.Service + const bus = yield* Bus.Service + const snapshot = yield* Snapshot.Service + const agents = yield* Agent.Service + const llm = yield* LLM.Service + const permission = yield* Permission.Service + const plugin = yield* Plugin.Service + const summary = yield* SessionSummary.Service + const scope = yield* Scope.Scope + const status = yield* SessionStatus.Service - const create = Effect.fn("SessionProcessor.create")(function* (input: Input) { - // Pre-capture snapshot before the LLM stream starts. The AI SDK - // may execute tools internally before emitting start-step events, - // so capturing inside the event handler can be too late. - const initialSnapshot = yield* snapshot.track() - const ctx: ProcessorContext = { - assistantMessage: input.assistantMessage, - sessionID: input.sessionID, - model: input.model, - toolcalls: {}, - shouldBreak: false, - snapshot: initialSnapshot, - blocked: false, - needsCompaction: false, - currentText: undefined, - reasoningMap: {}, - } - let aborted = false - const slog = log.clone().tag("sessionID", input.sessionID).tag("messageID", input.assistantMessage.id) + const create = Effect.fn("SessionProcessor.create")(function* (input: Input) { + // Pre-capture snapshot before the LLM stream starts. The AI SDK + // may execute tools internally before emitting start-step events, + // so capturing inside the event handler can be too late. + const initialSnapshot = yield* snapshot.track() + const ctx: ProcessorContext = { + assistantMessage: input.assistantMessage, + sessionID: input.sessionID, + model: input.model, + toolcalls: {}, + shouldBreak: false, + snapshot: initialSnapshot, + blocked: false, + needsCompaction: false, + currentText: undefined, + reasoningMap: {}, + } + let aborted = false + const slog = log.clone().tag("sessionID", input.sessionID).tag("messageID", input.assistantMessage.id) - const parse = (e: unknown) => - MessageV2.fromError(e, { - providerID: input.model.providerID, - aborted, - }) - - const settleToolCall = Effect.fn("SessionProcessor.settleToolCall")(function* (toolCallID: string) { - const done = ctx.toolcalls[toolCallID]?.done - delete ctx.toolcalls[toolCallID] - if (done) yield* Deferred.succeed(done, undefined).pipe(Effect.ignore) + const parse = (e: unknown) => + MessageV2.fromError(e, { + providerID: input.model.providerID, + aborted, }) - const readToolCall = Effect.fn("SessionProcessor.readToolCall")(function* (toolCallID: string) { - const call = ctx.toolcalls[toolCallID] - if (!call) return - const part = yield* session.getPart({ - partID: call.partID, - messageID: call.messageID, - sessionID: call.sessionID, - }) - if (!part || part.type !== "tool") { - delete ctx.toolcalls[toolCallID] - return - } - return { call, part } - }) - - const updateToolCall = Effect.fn("SessionProcessor.updateToolCall")(function* ( - toolCallID: string, - update: (part: MessageV2.ToolPart) => MessageV2.ToolPart, - ) { - const match = yield* readToolCall(toolCallID) - if (!match) return - const part = yield* session.updatePart(update(match.part)) - ctx.toolcalls[toolCallID] = { - ...match.call, - partID: part.id, - messageID: part.messageID, - sessionID: part.sessionID, - } - return part - }) - - const completeToolCall = Effect.fn("SessionProcessor.completeToolCall")(function* ( - toolCallID: string, - output: { - title: string - metadata: Record - output: string - attachments?: MessageV2.FilePart[] - }, - ) { - const match = yield* readToolCall(toolCallID) - if (!match || match.part.state.status !== "running") return - yield* session.updatePart({ - ...match.part, - state: { - status: "completed", - input: match.part.state.input, - output: output.output, - metadata: output.metadata, - title: output.title, - time: { start: match.part.state.time.start, end: Date.now() }, - attachments: output.attachments, - }, - }) - yield* settleToolCall(toolCallID) - }) - - const failToolCall = Effect.fn("SessionProcessor.failToolCall")(function* (toolCallID: string, error: unknown) { - const match = yield* readToolCall(toolCallID) - if (!match || match.part.state.status !== "running") return false - yield* session.updatePart({ - ...match.part, - state: { - status: "error", - input: match.part.state.input, - error: errorMessage(error), - time: { start: match.part.state.time.start, end: Date.now() }, - }, - }) - if (error instanceof Permission.RejectedError || error instanceof Question.RejectedError) { - ctx.blocked = ctx.shouldBreak - } - yield* settleToolCall(toolCallID) - return true - }) - - const handleEvent = Effect.fn("SessionProcessor.handleEvent")(function* (value: StreamEvent) { - switch (value.type) { - case "start": - yield* status.set(ctx.sessionID, { type: "busy" }) - return - - case "reasoning-start": - if (value.id in ctx.reasoningMap) return - ctx.reasoningMap[value.id] = { - id: PartID.ascending(), - messageID: ctx.assistantMessage.id, - sessionID: ctx.assistantMessage.sessionID, - type: "reasoning", - text: "", - time: { start: Date.now() }, - metadata: value.providerMetadata, - } - yield* session.updatePart(ctx.reasoningMap[value.id]) - return - - case "reasoning-delta": - if (!(value.id in ctx.reasoningMap)) return - ctx.reasoningMap[value.id].text += value.text - if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata - yield* session.updatePartDelta({ - sessionID: ctx.reasoningMap[value.id].sessionID, - messageID: ctx.reasoningMap[value.id].messageID, - partID: ctx.reasoningMap[value.id].id, - field: "text", - delta: value.text, - }) - return - - case "reasoning-end": - if (!(value.id in ctx.reasoningMap)) return - // oxlint-disable-next-line no-self-assign -- reactivity trigger - ctx.reasoningMap[value.id].text = ctx.reasoningMap[value.id].text - ctx.reasoningMap[value.id].time = { ...ctx.reasoningMap[value.id].time, end: Date.now() } - if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata - yield* session.updatePart(ctx.reasoningMap[value.id]) - delete ctx.reasoningMap[value.id] - return - - case "tool-input-start": - if (ctx.assistantMessage.summary) { - throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`) - } - const part = yield* session.updatePart({ - id: ctx.toolcalls[value.id]?.partID ?? PartID.ascending(), - messageID: ctx.assistantMessage.id, - sessionID: ctx.assistantMessage.sessionID, - type: "tool", - tool: value.toolName, - callID: value.id, - state: { status: "pending", input: {}, raw: "" }, - metadata: value.providerExecuted ? { providerExecuted: true } : undefined, - } satisfies MessageV2.ToolPart) - ctx.toolcalls[value.id] = { - done: yield* Deferred.make(), - partID: part.id, - messageID: part.messageID, - sessionID: part.sessionID, - } - return - - case "tool-input-delta": - return - - case "tool-input-end": - return - - case "tool-call": { - if (ctx.assistantMessage.summary) { - throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`) - } - yield* updateToolCall(value.toolCallId, (match) => ({ - ...match, - tool: value.toolName, - state: { - ...match.state, - status: "running", - input: value.input, - time: { start: Date.now() }, - }, - metadata: match.metadata?.providerExecuted - ? { ...value.providerMetadata, providerExecuted: true } - : value.providerMetadata, - })) - - const parts = MessageV2.parts(ctx.assistantMessage.id) - const recentParts = parts.slice(-DOOM_LOOP_THRESHOLD) - - if ( - recentParts.length !== DOOM_LOOP_THRESHOLD || - !recentParts.every( - (part) => - part.type === "tool" && - part.tool === value.toolName && - part.state.status !== "pending" && - JSON.stringify(part.state.input) === JSON.stringify(value.input), - ) - ) { - return - } - - const agent = yield* agents.get(ctx.assistantMessage.agent) - yield* permission.ask({ - permission: "doom_loop", - patterns: [value.toolName], - sessionID: ctx.assistantMessage.sessionID, - metadata: { tool: value.toolName, input: value.input }, - always: [value.toolName], - ruleset: agent.permission, - }) - return - } - - case "tool-result": { - yield* completeToolCall(value.toolCallId, value.output) - return - } - - case "tool-error": { - yield* failToolCall(value.toolCallId, value.error) - return - } - - case "error": - throw value.error - - case "start-step": - if (!ctx.snapshot) ctx.snapshot = yield* snapshot.track() - yield* session.updatePart({ - id: PartID.ascending(), - messageID: ctx.assistantMessage.id, - sessionID: ctx.sessionID, - snapshot: ctx.snapshot, - type: "step-start", - }) - return - - case "finish-step": { - const usage = Session.getUsage({ - model: ctx.model, - usage: value.usage, - metadata: value.providerMetadata, - }) - ctx.assistantMessage.finish = value.finishReason - ctx.assistantMessage.cost += usage.cost - ctx.assistantMessage.tokens = usage.tokens - yield* session.updatePart({ - id: PartID.ascending(), - reason: value.finishReason, - snapshot: yield* snapshot.track(), - messageID: ctx.assistantMessage.id, - sessionID: ctx.assistantMessage.sessionID, - type: "step-finish", - tokens: usage.tokens, - cost: usage.cost, - }) - yield* session.updateMessage(ctx.assistantMessage) - if (ctx.snapshot) { - const patch = yield* snapshot.patch(ctx.snapshot) - if (patch.files.length) { - yield* session.updatePart({ - id: PartID.ascending(), - messageID: ctx.assistantMessage.id, - sessionID: ctx.sessionID, - type: "patch", - hash: patch.hash, - files: patch.files, - }) - } - ctx.snapshot = undefined - } - yield* summary - .summarize({ - sessionID: ctx.sessionID, - messageID: ctx.assistantMessage.parentID, - }) - .pipe(Effect.ignore, Effect.forkIn(scope)) - if ( - !ctx.assistantMessage.summary && - isOverflow({ cfg: yield* config.get(), tokens: usage.tokens, model: ctx.model }) - ) { - ctx.needsCompaction = true - } - return - } - - case "text-start": - ctx.currentText = { - id: PartID.ascending(), - messageID: ctx.assistantMessage.id, - sessionID: ctx.assistantMessage.sessionID, - type: "text", - text: "", - time: { start: Date.now() }, - metadata: value.providerMetadata, - } - yield* session.updatePart(ctx.currentText) - return - - case "text-delta": - if (!ctx.currentText) return - ctx.currentText.text += value.text - if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata - yield* session.updatePartDelta({ - sessionID: ctx.currentText.sessionID, - messageID: ctx.currentText.messageID, - partID: ctx.currentText.id, - field: "text", - delta: value.text, - }) - return - - case "text-end": - if (!ctx.currentText) return - // oxlint-disable-next-line no-self-assign -- reactivity trigger - ctx.currentText.text = ctx.currentText.text - ctx.currentText.text = (yield* plugin.trigger( - "experimental.text.complete", - { - sessionID: ctx.sessionID, - messageID: ctx.assistantMessage.id, - partID: ctx.currentText.id, - }, - { text: ctx.currentText.text }, - )).text - { - const end = Date.now() - ctx.currentText.time = { start: ctx.currentText.time?.start ?? end, end } - } - if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata - yield* session.updatePart(ctx.currentText) - ctx.currentText = undefined - return - - case "finish": - return - - default: - slog.info("unhandled", { event: value.type, value }) - return - } - }) - - const cleanup = Effect.fn("SessionProcessor.cleanup")(function* () { - if (ctx.snapshot) { - const patch = yield* snapshot.patch(ctx.snapshot) - if (patch.files.length) { - yield* session.updatePart({ - id: PartID.ascending(), - messageID: ctx.assistantMessage.id, - sessionID: ctx.sessionID, - type: "patch", - hash: patch.hash, - files: patch.files, - }) - } - ctx.snapshot = undefined - } - - if (ctx.currentText) { - const end = Date.now() - ctx.currentText.time = { start: ctx.currentText.time?.start ?? end, end } - yield* session.updatePart(ctx.currentText) - ctx.currentText = undefined - } - - for (const part of Object.values(ctx.reasoningMap)) { - const end = Date.now() - yield* session.updatePart({ - ...part, - time: { start: part.time.start ?? end, end }, - }) - } - ctx.reasoningMap = {} - - yield* Effect.forEach( - Object.values(ctx.toolcalls), - (call) => Deferred.await(call.done).pipe(Effect.timeout("250 millis"), Effect.ignore), - { concurrency: "unbounded" }, - ) - - for (const toolCallID of Object.keys(ctx.toolcalls)) { - const match = yield* readToolCall(toolCallID) - if (!match) continue - const part = match.part - const end = Date.now() - const metadata = "metadata" in part.state && isRecord(part.state.metadata) ? part.state.metadata : {} - yield* session.updatePart({ - ...part, - state: { - ...part.state, - status: "error", - error: "Tool execution aborted", - metadata: { ...metadata, interrupted: true }, - time: { start: "time" in part.state ? part.state.time.start : end, end }, - }, - }) - } - ctx.toolcalls = {} - ctx.assistantMessage.time.completed = Date.now() - yield* session.updateMessage(ctx.assistantMessage) - }) - - const halt = Effect.fn("SessionProcessor.halt")(function* (e: unknown) { - slog.error("process", { error: errorMessage(e), stack: e instanceof Error ? e.stack : undefined }) - const error = parse(e) - if (MessageV2.ContextOverflowError.isInstance(error)) { - ctx.needsCompaction = true - yield* bus.publish(Session.Event.Error, { sessionID: ctx.sessionID, error }) - return - } - ctx.assistantMessage.error = error - yield* bus.publish(Session.Event.Error, { - sessionID: ctx.assistantMessage.sessionID, - error: ctx.assistantMessage.error, - }) - yield* status.set(ctx.sessionID, { type: "idle" }) - }) - - const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) { - slog.info("process") - ctx.needsCompaction = false - ctx.shouldBreak = (yield* config.get()).experimental?.continue_loop_on_deny !== true - - return yield* Effect.gen(function* () { - yield* Effect.gen(function* () { - ctx.currentText = undefined - ctx.reasoningMap = {} - const stream = llm.stream(streamInput) - - yield* stream.pipe( - Stream.tap((event) => handleEvent(event)), - Stream.takeUntil(() => ctx.needsCompaction), - Stream.runDrain, - ) - }).pipe( - Effect.onInterrupt(() => - Effect.gen(function* () { - aborted = true - if (!ctx.assistantMessage.error) { - yield* halt(new DOMException("Aborted", "AbortError")) - } - }), - ), - Effect.catchCauseIf( - (cause) => !Cause.hasInterruptsOnly(cause), - (cause) => Effect.fail(Cause.squash(cause)), - ), - Effect.retry( - SessionRetry.policy({ - parse, - set: (info) => - status.set(ctx.sessionID, { - type: "retry", - attempt: info.attempt, - message: info.message, - next: info.next, - }), - }), - ), - Effect.catch(halt), - Effect.ensuring(cleanup()), - ) - - if (ctx.needsCompaction) return "compact" - if (ctx.blocked || ctx.assistantMessage.error) return "stop" - return "continue" - }) - }) - - return { - get message() { - return ctx.assistantMessage - }, - updateToolCall, - completeToolCall, - process, - } satisfies Handle + const settleToolCall = Effect.fn("SessionProcessor.settleToolCall")(function* (toolCallID: string) { + const done = ctx.toolcalls[toolCallID]?.done + delete ctx.toolcalls[toolCallID] + if (done) yield* Deferred.succeed(done, undefined).pipe(Effect.ignore) }) - return Service.of({ create }) - }), - ) + const readToolCall = Effect.fn("SessionProcessor.readToolCall")(function* (toolCallID: string) { + const call = ctx.toolcalls[toolCallID] + if (!call) return + const part = yield* session.getPart({ + partID: call.partID, + messageID: call.messageID, + sessionID: call.sessionID, + }) + if (!part || part.type !== "tool") { + delete ctx.toolcalls[toolCallID] + return + } + return { call, part } + }) - export const defaultLayer = Layer.suspend(() => - layer.pipe( - Layer.provide(Session.defaultLayer), - Layer.provide(Snapshot.defaultLayer), - Layer.provide(Agent.defaultLayer), - Layer.provide(LLM.defaultLayer), - Layer.provide(Permission.defaultLayer), - Layer.provide(Plugin.defaultLayer), - Layer.provide(SessionSummary.defaultLayer), - Layer.provide(SessionStatus.defaultLayer), - Layer.provide(Bus.layer), - Layer.provide(Config.defaultLayer), - ), - ) -} + const updateToolCall = Effect.fn("SessionProcessor.updateToolCall")(function* ( + toolCallID: string, + update: (part: MessageV2.ToolPart) => MessageV2.ToolPart, + ) { + const match = yield* readToolCall(toolCallID) + if (!match) return + const part = yield* session.updatePart(update(match.part)) + ctx.toolcalls[toolCallID] = { + ...match.call, + partID: part.id, + messageID: part.messageID, + sessionID: part.sessionID, + } + return part + }) + + const completeToolCall = Effect.fn("SessionProcessor.completeToolCall")(function* ( + toolCallID: string, + output: { + title: string + metadata: Record + output: string + attachments?: MessageV2.FilePart[] + }, + ) { + const match = yield* readToolCall(toolCallID) + if (!match || match.part.state.status !== "running") return + yield* session.updatePart({ + ...match.part, + state: { + status: "completed", + input: match.part.state.input, + output: output.output, + metadata: output.metadata, + title: output.title, + time: { start: match.part.state.time.start, end: Date.now() }, + attachments: output.attachments, + }, + }) + yield* settleToolCall(toolCallID) + }) + + const failToolCall = Effect.fn("SessionProcessor.failToolCall")(function* (toolCallID: string, error: unknown) { + const match = yield* readToolCall(toolCallID) + if (!match || match.part.state.status !== "running") return false + yield* session.updatePart({ + ...match.part, + state: { + status: "error", + input: match.part.state.input, + error: errorMessage(error), + time: { start: match.part.state.time.start, end: Date.now() }, + }, + }) + if (error instanceof Permission.RejectedError || error instanceof Question.RejectedError) { + ctx.blocked = ctx.shouldBreak + } + yield* settleToolCall(toolCallID) + return true + }) + + const handleEvent = Effect.fn("SessionProcessor.handleEvent")(function* (value: StreamEvent) { + switch (value.type) { + case "start": + yield* status.set(ctx.sessionID, { type: "busy" }) + return + + case "reasoning-start": + if (value.id in ctx.reasoningMap) return + ctx.reasoningMap[value.id] = { + id: PartID.ascending(), + messageID: ctx.assistantMessage.id, + sessionID: ctx.assistantMessage.sessionID, + type: "reasoning", + text: "", + time: { start: Date.now() }, + metadata: value.providerMetadata, + } + yield* session.updatePart(ctx.reasoningMap[value.id]) + return + + case "reasoning-delta": + if (!(value.id in ctx.reasoningMap)) return + ctx.reasoningMap[value.id].text += value.text + if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata + yield* session.updatePartDelta({ + sessionID: ctx.reasoningMap[value.id].sessionID, + messageID: ctx.reasoningMap[value.id].messageID, + partID: ctx.reasoningMap[value.id].id, + field: "text", + delta: value.text, + }) + return + + case "reasoning-end": + if (!(value.id in ctx.reasoningMap)) return + // oxlint-disable-next-line no-self-assign -- reactivity trigger + ctx.reasoningMap[value.id].text = ctx.reasoningMap[value.id].text + ctx.reasoningMap[value.id].time = { ...ctx.reasoningMap[value.id].time, end: Date.now() } + if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata + yield* session.updatePart(ctx.reasoningMap[value.id]) + delete ctx.reasoningMap[value.id] + return + + case "tool-input-start": + if (ctx.assistantMessage.summary) { + throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`) + } + const part = yield* session.updatePart({ + id: ctx.toolcalls[value.id]?.partID ?? PartID.ascending(), + messageID: ctx.assistantMessage.id, + sessionID: ctx.assistantMessage.sessionID, + type: "tool", + tool: value.toolName, + callID: value.id, + state: { status: "pending", input: {}, raw: "" }, + metadata: value.providerExecuted ? { providerExecuted: true } : undefined, + } satisfies MessageV2.ToolPart) + ctx.toolcalls[value.id] = { + done: yield* Deferred.make(), + partID: part.id, + messageID: part.messageID, + sessionID: part.sessionID, + } + return + + case "tool-input-delta": + return + + case "tool-input-end": + return + + case "tool-call": { + if (ctx.assistantMessage.summary) { + throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`) + } + yield* updateToolCall(value.toolCallId, (match) => ({ + ...match, + tool: value.toolName, + state: { + ...match.state, + status: "running", + input: value.input, + time: { start: Date.now() }, + }, + metadata: match.metadata?.providerExecuted + ? { ...value.providerMetadata, providerExecuted: true } + : value.providerMetadata, + })) + + const parts = MessageV2.parts(ctx.assistantMessage.id) + const recentParts = parts.slice(-DOOM_LOOP_THRESHOLD) + + if ( + recentParts.length !== DOOM_LOOP_THRESHOLD || + !recentParts.every( + (part) => + part.type === "tool" && + part.tool === value.toolName && + part.state.status !== "pending" && + JSON.stringify(part.state.input) === JSON.stringify(value.input), + ) + ) { + return + } + + const agent = yield* agents.get(ctx.assistantMessage.agent) + yield* permission.ask({ + permission: "doom_loop", + patterns: [value.toolName], + sessionID: ctx.assistantMessage.sessionID, + metadata: { tool: value.toolName, input: value.input }, + always: [value.toolName], + ruleset: agent.permission, + }) + return + } + + case "tool-result": { + yield* completeToolCall(value.toolCallId, value.output) + return + } + + case "tool-error": { + yield* failToolCall(value.toolCallId, value.error) + return + } + + case "error": + throw value.error + + case "start-step": + if (!ctx.snapshot) ctx.snapshot = yield* snapshot.track() + yield* session.updatePart({ + id: PartID.ascending(), + messageID: ctx.assistantMessage.id, + sessionID: ctx.sessionID, + snapshot: ctx.snapshot, + type: "step-start", + }) + return + + case "finish-step": { + const usage = Session.getUsage({ + model: ctx.model, + usage: value.usage, + metadata: value.providerMetadata, + }) + ctx.assistantMessage.finish = value.finishReason + ctx.assistantMessage.cost += usage.cost + ctx.assistantMessage.tokens = usage.tokens + yield* session.updatePart({ + id: PartID.ascending(), + reason: value.finishReason, + snapshot: yield* snapshot.track(), + messageID: ctx.assistantMessage.id, + sessionID: ctx.assistantMessage.sessionID, + type: "step-finish", + tokens: usage.tokens, + cost: usage.cost, + }) + yield* session.updateMessage(ctx.assistantMessage) + if (ctx.snapshot) { + const patch = yield* snapshot.patch(ctx.snapshot) + if (patch.files.length) { + yield* session.updatePart({ + id: PartID.ascending(), + messageID: ctx.assistantMessage.id, + sessionID: ctx.sessionID, + type: "patch", + hash: patch.hash, + files: patch.files, + }) + } + ctx.snapshot = undefined + } + yield* summary + .summarize({ + sessionID: ctx.sessionID, + messageID: ctx.assistantMessage.parentID, + }) + .pipe(Effect.ignore, Effect.forkIn(scope)) + if ( + !ctx.assistantMessage.summary && + isOverflow({ cfg: yield* config.get(), tokens: usage.tokens, model: ctx.model }) + ) { + ctx.needsCompaction = true + } + return + } + + case "text-start": + ctx.currentText = { + id: PartID.ascending(), + messageID: ctx.assistantMessage.id, + sessionID: ctx.assistantMessage.sessionID, + type: "text", + text: "", + time: { start: Date.now() }, + metadata: value.providerMetadata, + } + yield* session.updatePart(ctx.currentText) + return + + case "text-delta": + if (!ctx.currentText) return + ctx.currentText.text += value.text + if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata + yield* session.updatePartDelta({ + sessionID: ctx.currentText.sessionID, + messageID: ctx.currentText.messageID, + partID: ctx.currentText.id, + field: "text", + delta: value.text, + }) + return + + case "text-end": + if (!ctx.currentText) return + // oxlint-disable-next-line no-self-assign -- reactivity trigger + ctx.currentText.text = ctx.currentText.text + ctx.currentText.text = (yield* plugin.trigger( + "experimental.text.complete", + { + sessionID: ctx.sessionID, + messageID: ctx.assistantMessage.id, + partID: ctx.currentText.id, + }, + { text: ctx.currentText.text }, + )).text + { + const end = Date.now() + ctx.currentText.time = { start: ctx.currentText.time?.start ?? end, end } + } + if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata + yield* session.updatePart(ctx.currentText) + ctx.currentText = undefined + return + + case "finish": + return + + default: + slog.info("unhandled", { event: value.type, value }) + return + } + }) + + const cleanup = Effect.fn("SessionProcessor.cleanup")(function* () { + if (ctx.snapshot) { + const patch = yield* snapshot.patch(ctx.snapshot) + if (patch.files.length) { + yield* session.updatePart({ + id: PartID.ascending(), + messageID: ctx.assistantMessage.id, + sessionID: ctx.sessionID, + type: "patch", + hash: patch.hash, + files: patch.files, + }) + } + ctx.snapshot = undefined + } + + if (ctx.currentText) { + const end = Date.now() + ctx.currentText.time = { start: ctx.currentText.time?.start ?? end, end } + yield* session.updatePart(ctx.currentText) + ctx.currentText = undefined + } + + for (const part of Object.values(ctx.reasoningMap)) { + const end = Date.now() + yield* session.updatePart({ + ...part, + time: { start: part.time.start ?? end, end }, + }) + } + ctx.reasoningMap = {} + + yield* Effect.forEach( + Object.values(ctx.toolcalls), + (call) => Deferred.await(call.done).pipe(Effect.timeout("250 millis"), Effect.ignore), + { concurrency: "unbounded" }, + ) + + for (const toolCallID of Object.keys(ctx.toolcalls)) { + const match = yield* readToolCall(toolCallID) + if (!match) continue + const part = match.part + const end = Date.now() + const metadata = "metadata" in part.state && isRecord(part.state.metadata) ? part.state.metadata : {} + yield* session.updatePart({ + ...part, + state: { + ...part.state, + status: "error", + error: "Tool execution aborted", + metadata: { ...metadata, interrupted: true }, + time: { start: "time" in part.state ? part.state.time.start : end, end }, + }, + }) + } + ctx.toolcalls = {} + ctx.assistantMessage.time.completed = Date.now() + yield* session.updateMessage(ctx.assistantMessage) + }) + + const halt = Effect.fn("SessionProcessor.halt")(function* (e: unknown) { + slog.error("process", { error: errorMessage(e), stack: e instanceof Error ? e.stack : undefined }) + const error = parse(e) + if (MessageV2.ContextOverflowError.isInstance(error)) { + ctx.needsCompaction = true + yield* bus.publish(Session.Event.Error, { sessionID: ctx.sessionID, error }) + return + } + ctx.assistantMessage.error = error + yield* bus.publish(Session.Event.Error, { + sessionID: ctx.assistantMessage.sessionID, + error: ctx.assistantMessage.error, + }) + yield* status.set(ctx.sessionID, { type: "idle" }) + }) + + const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) { + slog.info("process") + ctx.needsCompaction = false + ctx.shouldBreak = (yield* config.get()).experimental?.continue_loop_on_deny !== true + + return yield* Effect.gen(function* () { + yield* Effect.gen(function* () { + ctx.currentText = undefined + ctx.reasoningMap = {} + const stream = llm.stream(streamInput) + + yield* stream.pipe( + Stream.tap((event) => handleEvent(event)), + Stream.takeUntil(() => ctx.needsCompaction), + Stream.runDrain, + ) + }).pipe( + Effect.onInterrupt(() => + Effect.gen(function* () { + aborted = true + if (!ctx.assistantMessage.error) { + yield* halt(new DOMException("Aborted", "AbortError")) + } + }), + ), + Effect.catchCauseIf( + (cause) => !Cause.hasInterruptsOnly(cause), + (cause) => Effect.fail(Cause.squash(cause)), + ), + Effect.retry( + SessionRetry.policy({ + parse, + set: (info) => + status.set(ctx.sessionID, { + type: "retry", + attempt: info.attempt, + message: info.message, + next: info.next, + }), + }), + ), + Effect.catch(halt), + Effect.ensuring(cleanup()), + ) + + if (ctx.needsCompaction) return "compact" + if (ctx.blocked || ctx.assistantMessage.error) return "stop" + return "continue" + }) + }) + + return { + get message() { + return ctx.assistantMessage + }, + updateToolCall, + completeToolCall, + process, + } satisfies Handle + }) + + return Service.of({ create }) + }), +) + +export const defaultLayer = Layer.suspend(() => + layer.pipe( + Layer.provide(Session.defaultLayer), + Layer.provide(Snapshot.defaultLayer), + Layer.provide(Agent.defaultLayer), + Layer.provide(LLM.defaultLayer), + Layer.provide(Permission.defaultLayer), + Layer.provide(Plugin.defaultLayer), + Layer.provide(SessionSummary.defaultLayer), + Layer.provide(SessionStatus.defaultLayer), + Layer.provide(Bus.layer), + Layer.provide(Config.defaultLayer), + ), +) + +export * as SessionProcessor from "./processor" diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 004ee19abe..14fdf30780 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -64,221 +64,220 @@ IMPORTANT: const STRUCTURED_OUTPUT_SYSTEM_PROMPT = `IMPORTANT: The user has requested structured output. You MUST use the StructuredOutput tool to provide your final response. Do NOT respond with plain text - you MUST call the StructuredOutput tool with your answer formatted according to the schema.` -export namespace SessionPrompt { - const log = Log.create({ service: "session.prompt" }) - const elog = EffectLogger.create({ service: "session.prompt" }) +const log = Log.create({ service: "session.prompt" }) +const elog = EffectLogger.create({ service: "session.prompt" }) - export interface Interface { - readonly cancel: (sessionID: SessionID) => Effect.Effect - readonly prompt: (input: PromptInput) => Effect.Effect - readonly loop: (input: z.infer) => Effect.Effect - readonly shell: (input: ShellInput) => Effect.Effect - readonly command: (input: CommandInput) => Effect.Effect - readonly resolvePromptParts: (template: string) => Effect.Effect - } +export interface Interface { + readonly cancel: (sessionID: SessionID) => Effect.Effect + readonly prompt: (input: PromptInput) => Effect.Effect + readonly loop: (input: z.infer) => Effect.Effect + readonly shell: (input: ShellInput) => Effect.Effect + readonly command: (input: CommandInput) => Effect.Effect + readonly resolvePromptParts: (template: string) => Effect.Effect +} - export class Service extends Context.Service()("@opencode/SessionPrompt") {} +export class Service extends Context.Service()("@opencode/SessionPrompt") {} - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const bus = yield* Bus.Service - const status = yield* SessionStatus.Service - const sessions = yield* Session.Service - const agents = yield* Agent.Service - const provider = yield* Provider.Service - const processor = yield* SessionProcessor.Service - const compaction = yield* SessionCompaction.Service - const plugin = yield* Plugin.Service - const commands = yield* Command.Service - const permission = yield* Permission.Service - const fsys = yield* AppFileSystem.Service - const mcp = yield* MCP.Service - const lsp = yield* LSP.Service - const filetime = yield* FileTime.Service - const registry = yield* ToolRegistry.Service - const truncate = yield* Truncate.Service - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner - const scope = yield* Scope.Scope - const instruction = yield* Instruction.Service - const state = yield* SessionRunState.Service - const revert = yield* SessionRevert.Service - const summary = yield* SessionSummary.Service - const sys = yield* SystemPrompt.Service - const llm = yield* LLM.Service - const runner = Effect.fn("SessionPrompt.runner")(function* () { - return yield* EffectBridge.make() - }) - const ops = Effect.fn("SessionPrompt.ops")(function* () { - const run = yield* runner() - return { - cancel: (sessionID: SessionID) => run.fork(cancel(sessionID)), - resolvePromptParts: (template: string) => resolvePromptParts(template), - prompt: (input: PromptInput) => prompt(input), - } satisfies TaskPromptOps - }) +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const bus = yield* Bus.Service + const status = yield* SessionStatus.Service + const sessions = yield* Session.Service + const agents = yield* Agent.Service + const provider = yield* Provider.Service + const processor = yield* SessionProcessor.Service + const compaction = yield* SessionCompaction.Service + const plugin = yield* Plugin.Service + const commands = yield* Command.Service + const permission = yield* Permission.Service + const fsys = yield* AppFileSystem.Service + const mcp = yield* MCP.Service + const lsp = yield* LSP.Service + const filetime = yield* FileTime.Service + const registry = yield* ToolRegistry.Service + const truncate = yield* Truncate.Service + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const scope = yield* Scope.Scope + const instruction = yield* Instruction.Service + const state = yield* SessionRunState.Service + const revert = yield* SessionRevert.Service + const summary = yield* SessionSummary.Service + const sys = yield* SystemPrompt.Service + const llm = yield* LLM.Service + const runner = Effect.fn("SessionPrompt.runner")(function* () { + return yield* EffectBridge.make() + }) + const ops = Effect.fn("SessionPrompt.ops")(function* () { + const run = yield* runner() + return { + cancel: (sessionID: SessionID) => run.fork(cancel(sessionID)), + resolvePromptParts: (template: string) => resolvePromptParts(template), + prompt: (input: PromptInput) => prompt(input), + } satisfies TaskPromptOps + }) - const cancel = Effect.fn("SessionPrompt.cancel")(function* (sessionID: SessionID) { - yield* elog.info("cancel", { sessionID }) - yield* state.cancel(sessionID) - }) + const cancel = Effect.fn("SessionPrompt.cancel")(function* (sessionID: SessionID) { + yield* elog.info("cancel", { sessionID }) + yield* state.cancel(sessionID) + }) - const resolvePromptParts = Effect.fn("SessionPrompt.resolvePromptParts")(function* (template: string) { - const ctx = yield* InstanceState.context - const parts: PromptInput["parts"] = [{ type: "text", text: template }] - const files = ConfigMarkdown.files(template) - const seen = new Set() - yield* Effect.forEach( - files, - Effect.fnUntraced(function* (match) { - const name = match[1] - if (seen.has(name)) return - seen.add(name) - const filepath = name.startsWith("~/") - ? path.join(os.homedir(), name.slice(2)) - : path.resolve(ctx.worktree, name) + const resolvePromptParts = Effect.fn("SessionPrompt.resolvePromptParts")(function* (template: string) { + const ctx = yield* InstanceState.context + const parts: PromptInput["parts"] = [{ type: "text", text: template }] + const files = ConfigMarkdown.files(template) + const seen = new Set() + yield* Effect.forEach( + files, + Effect.fnUntraced(function* (match) { + const name = match[1] + if (seen.has(name)) return + seen.add(name) + const filepath = name.startsWith("~/") + ? path.join(os.homedir(), name.slice(2)) + : path.resolve(ctx.worktree, name) - const info = yield* fsys.stat(filepath).pipe(Effect.option) - if (Option.isNone(info)) { - const found = yield* agents.get(name) - if (found) parts.push({ type: "agent", name: found.name }) - return - } - const stat = info.value - parts.push({ - type: "file", - url: pathToFileURL(filepath).href, - filename: name, - mime: stat.type === "Directory" ? "application/x-directory" : "text/plain", - }) - }), - { concurrency: "unbounded", discard: true }, - ) - return parts - }) - - const title = Effect.fn("SessionPrompt.ensureTitle")(function* (input: { - session: Session.Info - history: MessageV2.WithParts[] - providerID: ProviderID - modelID: ModelID - }) { - if (input.session.parentID) return - if (!Session.isDefaultTitle(input.session.title)) return - - const real = (m: MessageV2.WithParts) => - m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic) - const idx = input.history.findIndex(real) - if (idx === -1) return - if (input.history.filter(real).length !== 1) return - - const context = input.history.slice(0, idx + 1) - const firstUser = context[idx] - if (!firstUser || firstUser.info.role !== "user") return - const firstInfo = firstUser.info - - const subtasks = firstUser.parts.filter((p): p is MessageV2.SubtaskPart => p.type === "subtask") - const onlySubtasks = subtasks.length > 0 && firstUser.parts.every((p) => p.type === "subtask") - - const ag = yield* agents.get("title") - if (!ag) return - const mdl = ag.model - ? yield* provider.getModel(ag.model.providerID, ag.model.modelID) - : ((yield* provider.getSmallModel(input.providerID)) ?? - (yield* provider.getModel(input.providerID, input.modelID))) - const msgs = onlySubtasks - ? [{ role: "user" as const, content: subtasks.map((p) => p.prompt).join("\n") }] - : yield* MessageV2.toModelMessagesEffect(context, mdl) - const text = yield* llm - .stream({ - agent: ag, - user: firstInfo, - system: [], - small: true, - tools: {}, - model: mdl, - sessionID: input.session.id, - retries: 2, - messages: [{ role: "user", content: "Generate a title for this conversation:\n" }, ...msgs], + const info = yield* fsys.stat(filepath).pipe(Effect.option) + if (Option.isNone(info)) { + const found = yield* agents.get(name) + if (found) parts.push({ type: "agent", name: found.name }) + return + } + const stat = info.value + parts.push({ + type: "file", + url: pathToFileURL(filepath).href, + filename: name, + mime: stat.type === "Directory" ? "application/x-directory" : "text/plain", }) - .pipe( - Stream.filter((e): e is Extract => e.type === "text-delta"), - Stream.map((e) => e.text), - Stream.mkString, - Effect.orDie, - ) - const cleaned = text - .replace(/[\s\S]*?<\/think>\s*/g, "") - .split("\n") - .map((line) => line.trim()) - .find((line) => line.length > 0) - if (!cleaned) return - const t = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned - yield* sessions - .setTitle({ sessionID: input.session.id, title: t }) - .pipe(Effect.catchCause((cause) => elog.error("failed to generate title", { error: Cause.squash(cause) }))) - }) + }), + { concurrency: "unbounded", discard: true }, + ) + return parts + }) - const insertReminders = Effect.fn("SessionPrompt.insertReminders")(function* (input: { - messages: MessageV2.WithParts[] - agent: Agent.Info - session: Session.Info - }) { - const userMessage = input.messages.findLast((msg) => msg.info.role === "user") - if (!userMessage) return input.messages + const title = Effect.fn("SessionPrompt.ensureTitle")(function* (input: { + session: Session.Info + history: MessageV2.WithParts[] + providerID: ProviderID + modelID: ModelID + }) { + if (input.session.parentID) return + if (!Session.isDefaultTitle(input.session.title)) return - if (!Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE) { - if (input.agent.name === "plan") { - userMessage.parts.push({ - id: PartID.ascending(), - messageID: userMessage.info.id, - sessionID: userMessage.info.sessionID, - type: "text", - text: PROMPT_PLAN, - synthetic: true, - }) - } - const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan") - if (wasPlan && input.agent.name === "build") { - userMessage.parts.push({ - id: PartID.ascending(), - messageID: userMessage.info.id, - sessionID: userMessage.info.sessionID, - type: "text", - text: BUILD_SWITCH, - synthetic: true, - }) - } - return input.messages - } + const real = (m: MessageV2.WithParts) => + m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic) + const idx = input.history.findIndex(real) + if (idx === -1) return + if (input.history.filter(real).length !== 1) return - const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant") - if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") { - const plan = Session.plan(input.session) - if (!(yield* fsys.existsSafe(plan))) return input.messages - const part = yield* sessions.updatePart({ + const context = input.history.slice(0, idx + 1) + const firstUser = context[idx] + if (!firstUser || firstUser.info.role !== "user") return + const firstInfo = firstUser.info + + const subtasks = firstUser.parts.filter((p): p is MessageV2.SubtaskPart => p.type === "subtask") + const onlySubtasks = subtasks.length > 0 && firstUser.parts.every((p) => p.type === "subtask") + + const ag = yield* agents.get("title") + if (!ag) return + const mdl = ag.model + ? yield* provider.getModel(ag.model.providerID, ag.model.modelID) + : ((yield* provider.getSmallModel(input.providerID)) ?? + (yield* provider.getModel(input.providerID, input.modelID))) + const msgs = onlySubtasks + ? [{ role: "user" as const, content: subtasks.map((p) => p.prompt).join("\n") }] + : yield* MessageV2.toModelMessagesEffect(context, mdl) + const text = yield* llm + .stream({ + agent: ag, + user: firstInfo, + system: [], + small: true, + tools: {}, + model: mdl, + sessionID: input.session.id, + retries: 2, + messages: [{ role: "user", content: "Generate a title for this conversation:\n" }, ...msgs], + }) + .pipe( + Stream.filter((e): e is Extract => e.type === "text-delta"), + Stream.map((e) => e.text), + Stream.mkString, + Effect.orDie, + ) + const cleaned = text + .replace(/[\s\S]*?<\/think>\s*/g, "") + .split("\n") + .map((line) => line.trim()) + .find((line) => line.length > 0) + if (!cleaned) return + const t = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned + yield* sessions + .setTitle({ sessionID: input.session.id, title: t }) + .pipe(Effect.catchCause((cause) => elog.error("failed to generate title", { error: Cause.squash(cause) }))) + }) + + const insertReminders = Effect.fn("SessionPrompt.insertReminders")(function* (input: { + messages: MessageV2.WithParts[] + agent: Agent.Info + session: Session.Info + }) { + const userMessage = input.messages.findLast((msg) => msg.info.role === "user") + if (!userMessage) return input.messages + + if (!Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE) { + if (input.agent.name === "plan") { + userMessage.parts.push({ id: PartID.ascending(), messageID: userMessage.info.id, sessionID: userMessage.info.sessionID, type: "text", - text: `${BUILD_SWITCH}\n\nA plan file exists at ${plan}. You should execute on the plan defined within it`, + text: PROMPT_PLAN, synthetic: true, }) - userMessage.parts.push(part) - return input.messages } + const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan") + if (wasPlan && input.agent.name === "build") { + userMessage.parts.push({ + id: PartID.ascending(), + messageID: userMessage.info.id, + sessionID: userMessage.info.sessionID, + type: "text", + text: BUILD_SWITCH, + synthetic: true, + }) + } + return input.messages + } - if (input.agent.name !== "plan" || assistantMessage?.info.agent === "plan") return input.messages - + const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant") + if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") { const plan = Session.plan(input.session) - const exists = yield* fsys.existsSafe(plan) - if (!exists) yield* fsys.ensureDir(path.dirname(plan)).pipe(Effect.catch(Effect.die)) + if (!(yield* fsys.existsSafe(plan))) return input.messages const part = yield* sessions.updatePart({ id: PartID.ascending(), messageID: userMessage.info.id, sessionID: userMessage.info.sessionID, type: "text", - text: ` + text: `${BUILD_SWITCH}\n\nA plan file exists at ${plan}. You should execute on the plan defined within it`, + synthetic: true, + }) + userMessage.parts.push(part) + return input.messages + } + + if (input.agent.name !== "plan" || assistantMessage?.info.agent === "plan") return input.messages + + const plan = Session.plan(input.session) + const exists = yield* fsys.existsSafe(plan) + if (!exists) yield* fsys.ensureDir(path.dirname(plan)).pipe(Effect.catch(Effect.die)) + const part = yield* sessions.updatePart({ + id: PartID.ascending(), + messageID: userMessage.info.id, + sessionID: userMessage.info.sessionID, + type: "text", + text: ` Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received. ## Plan File Info: @@ -293,10 +292,10 @@ Goal: Gain a comprehensive understanding of the user's request by reading throug 1. Focus on understanding the user's request and the code associated with their request 2. **Launch up to 3 explore agents IN PARALLEL** (single message, multiple tool calls) to efficiently explore the codebase. - - Use 1 agent when the task is isolated to known files, the user provided specific file paths, or you're making a small targeted change. - - Use multiple agents when: the scope is uncertain, multiple areas of the codebase are involved, or you need to understand existing patterns before planning. - - Quality over quantity - 3 agents maximum, but you should try to use the minimum number of agents necessary (usually just 1) - - If using multiple agents: Provide each agent with a specific search focus or area to explore. Example: One agent searches for existing implementations, another explores related components, a third investigates testing patterns + - Use 1 agent when the task is isolated to known files, the user provided specific file paths, or you're making a small targeted change. + - Use multiple agents when: the scope is uncertain, multiple areas of the codebase are involved, or you need to understand existing patterns before planning. + - Quality over quantity - 3 agents maximum, but you should try to use the minimum number of agents necessary (usually just 1) + - If using multiple agents: Provide each agent with a specific search focus or area to explore. Example: One agent searches for existing implementations, another explores related components, a third investigates testing patterns 3. After exploring the code, use the question tool to clarify ambiguities in the user request up front. @@ -348,1507 +347,1508 @@ This is critical - your turn should only end with either asking the user a quest NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins. `, - synthetic: true, - }) - userMessage.parts.push(part) - return input.messages + synthetic: true, + }) + userMessage.parts.push(part) + return input.messages + }) + + const resolveTools = Effect.fn("SessionPrompt.resolveTools")(function* (input: { + agent: Agent.Info + model: Provider.Model + session: Session.Info + tools?: Record + processor: Pick + bypassAgentCheck: boolean + messages: MessageV2.WithParts[] + }) { + using _ = log.time("resolveTools") + const tools: Record = {} + const run = yield* runner() + const promptOps = yield* ops() + + const context = (args: any, options: ToolExecutionOptions): Tool.Context => ({ + sessionID: input.session.id, + abort: options.abortSignal!, + messageID: input.processor.message.id, + callID: options.toolCallId, + extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck, promptOps }, + agent: input.agent.name, + messages: input.messages, + metadata: (val) => + input.processor.updateToolCall(options.toolCallId, (match) => { + if (!["running", "pending"].includes(match.state.status)) return match + return { + ...match, + state: { + title: val.title, + metadata: val.metadata, + status: "running", + input: args, + time: { start: Date.now() }, + }, + } + }), + ask: (req) => + permission + .ask({ + ...req, + sessionID: input.session.id, + tool: { messageID: input.processor.message.id, callID: options.toolCallId }, + ruleset: Permission.merge(input.agent.permission, input.session.permission ?? []), + }) + .pipe(Effect.orDie), }) - const resolveTools = Effect.fn("SessionPrompt.resolveTools")(function* (input: { - agent: Agent.Info - model: Provider.Model - session: Session.Info - tools?: Record - processor: Pick - bypassAgentCheck: boolean - messages: MessageV2.WithParts[] - }) { - using _ = log.time("resolveTools") - const tools: Record = {} - const run = yield* runner() - const promptOps = yield* ops() - - const context = (args: any, options: ToolExecutionOptions): Tool.Context => ({ - sessionID: input.session.id, - abort: options.abortSignal!, - messageID: input.processor.message.id, - callID: options.toolCallId, - extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck, promptOps }, - agent: input.agent.name, - messages: input.messages, - metadata: (val) => - input.processor.updateToolCall(options.toolCallId, (match) => { - if (!["running", "pending"].includes(match.state.status)) return match - return { - ...match, - state: { - title: val.title, - metadata: val.metadata, - status: "running", - input: args, - time: { start: Date.now() }, - }, - } - }), - ask: (req) => - permission - .ask({ - ...req, - sessionID: input.session.id, - tool: { messageID: input.processor.message.id, callID: options.toolCallId }, - ruleset: Permission.merge(input.agent.permission, input.session.permission ?? []), - }) - .pipe(Effect.orDie), - }) - - for (const item of yield* registry.tools({ - modelID: ModelID.make(input.model.api.id), - providerID: input.model.providerID, - agent: input.agent, - })) { - const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters)) - tools[item.id] = tool({ - description: item.description, - inputSchema: jsonSchema(schema), - execute(args, options) { - return run.promise( - Effect.gen(function* () { - const ctx = context(args, options) - yield* plugin.trigger( - "tool.execute.before", - { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID }, - { args }, - ) - const result = yield* item.execute(args, ctx) - const output = { - ...result, - attachments: result.attachments?.map((attachment) => ({ - ...attachment, - id: PartID.ascending(), - sessionID: ctx.sessionID, - messageID: input.processor.message.id, - })), - } - yield* plugin.trigger( - "tool.execute.after", - { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID, args }, - output, - ) - if (options.abortSignal?.aborted) { - yield* input.processor.completeToolCall(options.toolCallId, output) - } - return output - }), - ) - }, - }) - } - - for (const [key, item] of Object.entries(yield* mcp.tools())) { - const execute = item.execute - if (!execute) continue - - const schema = yield* Effect.promise(() => Promise.resolve(asSchema(item.inputSchema).jsonSchema)) - const transformed = ProviderTransform.schema(input.model, schema) - item.inputSchema = jsonSchema(transformed) - item.execute = (args, opts) => - run.promise( + for (const item of yield* registry.tools({ + modelID: ModelID.make(input.model.api.id), + providerID: input.model.providerID, + agent: input.agent, + })) { + const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters)) + tools[item.id] = tool({ + description: item.description, + inputSchema: jsonSchema(schema), + execute(args, options) { + return run.promise( Effect.gen(function* () { - const ctx = context(args, opts) + const ctx = context(args, options) yield* plugin.trigger( "tool.execute.before", - { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId }, + { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID }, { args }, ) - yield* ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] }) - const result: Awaited>> = yield* Effect.promise(() => - execute(args, opts), - ) - yield* plugin.trigger( - "tool.execute.after", - { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId, args }, - result, - ) - - const textParts: string[] = [] - const attachments: Omit[] = [] - for (const contentItem of result.content) { - if (contentItem.type === "text") textParts.push(contentItem.text) - else if (contentItem.type === "image") { - attachments.push({ - type: "file", - mime: contentItem.mimeType, - url: `data:${contentItem.mimeType};base64,${contentItem.data}`, - }) - } else if (contentItem.type === "resource") { - const { resource } = contentItem - if (resource.text) textParts.push(resource.text) - if (resource.blob) { - attachments.push({ - type: "file", - mime: resource.mimeType ?? "application/octet-stream", - url: `data:${resource.mimeType ?? "application/octet-stream"};base64,${resource.blob}`, - filename: resource.uri, - }) - } - } - } - - const truncated = yield* truncate.output(textParts.join("\n\n"), {}, input.agent) - const metadata = { - ...result.metadata, - truncated: truncated.truncated, - ...(truncated.truncated && { outputPath: truncated.outputPath }), - } - + const result = yield* item.execute(args, ctx) const output = { - title: "", - metadata, - output: truncated.content, - attachments: attachments.map((attachment) => ({ + ...result, + attachments: result.attachments?.map((attachment) => ({ ...attachment, id: PartID.ascending(), sessionID: ctx.sessionID, messageID: input.processor.message.id, })), - content: result.content, } - if (opts.abortSignal?.aborted) { - yield* input.processor.completeToolCall(opts.toolCallId, output) + yield* plugin.trigger( + "tool.execute.after", + { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID, args }, + output, + ) + if (options.abortSignal?.aborted) { + yield* input.processor.completeToolCall(options.toolCallId, output) } return output }), ) - tools[key] = item - } - - return tools - }) - - const handleSubtask = Effect.fn("SessionPrompt.handleSubtask")(function* (input: { - task: MessageV2.SubtaskPart - model: Provider.Model - lastUser: MessageV2.User - sessionID: SessionID - session: Session.Info - msgs: MessageV2.WithParts[] - }) { - const { task, model, lastUser, sessionID, session, msgs } = input - const ctx = yield* InstanceState.context - const promptOps = yield* ops() - const { task: taskTool } = yield* registry.named() - const taskModel = task.model ? yield* getModel(task.model.providerID, task.model.modelID, sessionID) : model - const assistantMessage: MessageV2.Assistant = yield* sessions.updateMessage({ - id: MessageID.ascending(), - role: "assistant", - parentID: lastUser.id, - sessionID, - mode: task.agent, - agent: task.agent, - variant: lastUser.model.variant, - path: { cwd: ctx.directory, root: ctx.worktree }, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - modelID: taskModel.id, - providerID: taskModel.providerID, - time: { created: Date.now() }, - }) - let part: MessageV2.ToolPart = yield* sessions.updatePart({ - id: PartID.ascending(), - messageID: assistantMessage.id, - sessionID: assistantMessage.sessionID, - type: "tool", - callID: ulid(), - tool: TaskTool.id, - state: { - status: "running", - input: { - prompt: task.prompt, - description: task.description, - subagent_type: task.agent, - command: task.command, - }, - time: { start: Date.now() }, }, }) - const taskArgs = { - prompt: task.prompt, - description: task.description, - subagent_type: task.agent, - command: task.command, - } - yield* plugin.trigger( - "tool.execute.before", - { tool: TaskTool.id, sessionID, callID: part.id }, - { args: taskArgs }, - ) + } - const taskAgent = yield* agents.get(task.agent) - if (!taskAgent) { - const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) - const hint = available.length ? ` Available agents: ${available.join(", ")}` : "" - const error = new NamedError.Unknown({ message: `Agent not found: "${task.agent}".${hint}` }) - yield* bus.publish(Session.Event.Error, { sessionID, error: error.toObject() }) - throw error - } + for (const [key, item] of Object.entries(yield* mcp.tools())) { + const execute = item.execute + if (!execute) continue - let error: Error | undefined - const taskAbort = new AbortController() - const result = yield* taskTool - .execute(taskArgs, { - agent: task.agent, - messageID: assistantMessage.id, - sessionID, - abort: taskAbort.signal, - callID: part.callID, - extra: { bypassAgentCheck: true, promptOps }, - messages: msgs, - metadata: (val: { title?: string; metadata?: Record }) => - Effect.gen(function* () { - part = yield* sessions.updatePart({ - ...part, - type: "tool", - state: { ...part.state, ...val }, - } satisfies MessageV2.ToolPart) - }), - ask: (req: any) => - permission - .ask({ - ...req, - sessionID, - ruleset: Permission.merge(taskAgent.permission, session.permission ?? []), - }) - .pipe(Effect.orDie), - }) - .pipe( - Effect.catchCause((cause) => { - const defect = Cause.squash(cause) - error = defect instanceof Error ? defect : new Error(String(defect)) - log.error("subtask execution failed", { error, agent: task.agent, description: task.description }) - return Effect.void - }), - Effect.onInterrupt(() => - Effect.gen(function* () { - taskAbort.abort() - assistantMessage.finish = "tool-calls" - assistantMessage.time.completed = Date.now() - yield* sessions.updateMessage(assistantMessage) - if (part.state.status === "running") { - yield* sessions.updatePart({ - ...part, - state: { - status: "error", - error: "Cancelled", - time: { start: part.state.time.start, end: Date.now() }, - metadata: part.state.metadata, - input: part.state.input, - }, - } satisfies MessageV2.ToolPart) + const schema = yield* Effect.promise(() => Promise.resolve(asSchema(item.inputSchema).jsonSchema)) + const transformed = ProviderTransform.schema(input.model, schema) + item.inputSchema = jsonSchema(transformed) + item.execute = (args, opts) => + run.promise( + Effect.gen(function* () { + const ctx = context(args, opts) + yield* plugin.trigger( + "tool.execute.before", + { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId }, + { args }, + ) + yield* ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] }) + const result: Awaited>> = yield* Effect.promise(() => + execute(args, opts), + ) + yield* plugin.trigger( + "tool.execute.after", + { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId, args }, + result, + ) + + const textParts: string[] = [] + const attachments: Omit[] = [] + for (const contentItem of result.content) { + if (contentItem.type === "text") textParts.push(contentItem.text) + else if (contentItem.type === "image") { + attachments.push({ + type: "file", + mime: contentItem.mimeType, + url: `data:${contentItem.mimeType};base64,${contentItem.data}`, + }) + } else if (contentItem.type === "resource") { + const { resource } = contentItem + if (resource.text) textParts.push(resource.text) + if (resource.blob) { + attachments.push({ + type: "file", + mime: resource.mimeType ?? "application/octet-stream", + url: `data:${resource.mimeType ?? "application/octet-stream"};base64,${resource.blob}`, + filename: resource.uri, + }) + } } - }), - ), + } + + const truncated = yield* truncate.output(textParts.join("\n\n"), {}, input.agent) + const metadata = { + ...result.metadata, + truncated: truncated.truncated, + ...(truncated.truncated && { outputPath: truncated.outputPath }), + } + + const output = { + title: "", + metadata, + output: truncated.content, + attachments: attachments.map((attachment) => ({ + ...attachment, + id: PartID.ascending(), + sessionID: ctx.sessionID, + messageID: input.processor.message.id, + })), + content: result.content, + } + if (opts.abortSignal?.aborted) { + yield* input.processor.completeToolCall(opts.toolCallId, output) + } + return output + }), ) + tools[key] = item + } - const attachments = result?.attachments?.map((attachment) => ({ - ...attachment, - id: PartID.ascending(), - sessionID, + return tools + }) + + const handleSubtask = Effect.fn("SessionPrompt.handleSubtask")(function* (input: { + task: MessageV2.SubtaskPart + model: Provider.Model + lastUser: MessageV2.User + sessionID: SessionID + session: Session.Info + msgs: MessageV2.WithParts[] + }) { + const { task, model, lastUser, sessionID, session, msgs } = input + const ctx = yield* InstanceState.context + const promptOps = yield* ops() + const { task: taskTool } = yield* registry.named() + const taskModel = task.model ? yield* getModel(task.model.providerID, task.model.modelID, sessionID) : model + const assistantMessage: MessageV2.Assistant = yield* sessions.updateMessage({ + id: MessageID.ascending(), + role: "assistant", + parentID: lastUser.id, + sessionID, + mode: task.agent, + agent: task.agent, + variant: lastUser.model.variant, + path: { cwd: ctx.directory, root: ctx.worktree }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + modelID: taskModel.id, + providerID: taskModel.providerID, + time: { created: Date.now() }, + }) + let part: MessageV2.ToolPart = yield* sessions.updatePart({ + id: PartID.ascending(), + messageID: assistantMessage.id, + sessionID: assistantMessage.sessionID, + type: "tool", + callID: ulid(), + tool: TaskTool.id, + state: { + status: "running", + input: { + prompt: task.prompt, + description: task.description, + subagent_type: task.agent, + command: task.command, + }, + time: { start: Date.now() }, + }, + }) + const taskArgs = { + prompt: task.prompt, + description: task.description, + subagent_type: task.agent, + command: task.command, + } + yield* plugin.trigger( + "tool.execute.before", + { tool: TaskTool.id, sessionID, callID: part.id }, + { args: taskArgs }, + ) + + const taskAgent = yield* agents.get(task.agent) + if (!taskAgent) { + const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) + const hint = available.length ? ` Available agents: ${available.join(", ")}` : "" + const error = new NamedError.Unknown({ message: `Agent not found: "${task.agent}".${hint}` }) + yield* bus.publish(Session.Event.Error, { sessionID, error: error.toObject() }) + throw error + } + + let error: Error | undefined + const taskAbort = new AbortController() + const result = yield* taskTool + .execute(taskArgs, { + agent: task.agent, messageID: assistantMessage.id, - })) - - yield* plugin.trigger( - "tool.execute.after", - { tool: TaskTool.id, sessionID, callID: part.id, args: taskArgs }, - result, + sessionID, + abort: taskAbort.signal, + callID: part.callID, + extra: { bypassAgentCheck: true, promptOps }, + messages: msgs, + metadata: (val: { title?: string; metadata?: Record }) => + Effect.gen(function* () { + part = yield* sessions.updatePart({ + ...part, + type: "tool", + state: { ...part.state, ...val }, + } satisfies MessageV2.ToolPart) + }), + ask: (req: any) => + permission + .ask({ + ...req, + sessionID, + ruleset: Permission.merge(taskAgent.permission, session.permission ?? []), + }) + .pipe(Effect.orDie), + }) + .pipe( + Effect.catchCause((cause) => { + const defect = Cause.squash(cause) + error = defect instanceof Error ? defect : new Error(String(defect)) + log.error("subtask execution failed", { error, agent: task.agent, description: task.description }) + return Effect.void + }), + Effect.onInterrupt(() => + Effect.gen(function* () { + taskAbort.abort() + assistantMessage.finish = "tool-calls" + assistantMessage.time.completed = Date.now() + yield* sessions.updateMessage(assistantMessage) + if (part.state.status === "running") { + yield* sessions.updatePart({ + ...part, + state: { + status: "error", + error: "Cancelled", + time: { start: part.state.time.start, end: Date.now() }, + metadata: part.state.metadata, + input: part.state.input, + }, + } satisfies MessageV2.ToolPart) + } + }), + ), ) - assistantMessage.finish = "tool-calls" - assistantMessage.time.completed = Date.now() - yield* sessions.updateMessage(assistantMessage) + const attachments = result?.attachments?.map((attachment) => ({ + ...attachment, + id: PartID.ascending(), + sessionID, + messageID: assistantMessage.id, + })) - if (result && part.state.status === "running") { - yield* sessions.updatePart({ - ...part, - state: { - status: "completed", - input: part.state.input, - title: result.title, - metadata: result.metadata, - output: result.output, - attachments, - time: { ...part.state.time, end: Date.now() }, - }, - } satisfies MessageV2.ToolPart) - } + yield* plugin.trigger( + "tool.execute.after", + { tool: TaskTool.id, sessionID, callID: part.id, args: taskArgs }, + result, + ) - if (!result) { - yield* sessions.updatePart({ - ...part, - state: { - status: "error", - error: error ? `Tool execution failed: ${error.message}` : "Tool execution failed", - time: { - start: part.state.status === "running" ? part.state.time.start : Date.now(), - end: Date.now(), - }, - metadata: part.state.status === "pending" ? undefined : part.state.metadata, - input: part.state.input, - }, - } satisfies MessageV2.ToolPart) - } + assistantMessage.finish = "tool-calls" + assistantMessage.time.completed = Date.now() + yield* sessions.updateMessage(assistantMessage) - if (!task.command) return - - const summaryUserMsg: MessageV2.User = { - id: MessageID.ascending(), - sessionID, - role: "user", - time: { created: Date.now() }, - agent: lastUser.agent, - model: lastUser.model, - } - yield* sessions.updateMessage(summaryUserMsg) + if (result && part.state.status === "running") { yield* sessions.updatePart({ - id: PartID.ascending(), - messageID: summaryUserMsg.id, - sessionID, - type: "text", - text: "Summarize the task tool output above and continue with your task.", - synthetic: true, - } satisfies MessageV2.TextPart) + ...part, + state: { + status: "completed", + input: part.state.input, + title: result.title, + metadata: result.metadata, + output: result.output, + attachments, + time: { ...part.state.time, end: Date.now() }, + }, + } satisfies MessageV2.ToolPart) + } + + if (!result) { + yield* sessions.updatePart({ + ...part, + state: { + status: "error", + error: error ? `Tool execution failed: ${error.message}` : "Tool execution failed", + time: { + start: part.state.status === "running" ? part.state.time.start : Date.now(), + end: Date.now(), + }, + metadata: part.state.status === "pending" ? undefined : part.state.metadata, + input: part.state.input, + }, + } satisfies MessageV2.ToolPart) + } + + if (!task.command) return + + const summaryUserMsg: MessageV2.User = { + id: MessageID.ascending(), + sessionID, + role: "user", + time: { created: Date.now() }, + agent: lastUser.agent, + model: lastUser.model, + } + yield* sessions.updateMessage(summaryUserMsg) + yield* sessions.updatePart({ + id: PartID.ascending(), + messageID: summaryUserMsg.id, + sessionID, + type: "text", + text: "Summarize the task tool output above and continue with your task.", + synthetic: true, + } satisfies MessageV2.TextPart) + }) + + const shellImpl = Effect.fn("SessionPrompt.shellImpl")(function* (input: ShellInput) { + const ctx = yield* InstanceState.context + const run = yield* runner() + const session = yield* sessions.get(input.sessionID) + if (session.revert) { + yield* revert.cleanup(session) + } + const agent = yield* agents.get(input.agent) + if (!agent) { + const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) + const hint = available.length ? ` Available agents: ${available.join(", ")}` : "" + const error = new NamedError.Unknown({ message: `Agent not found: "${input.agent}".${hint}` }) + yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() }) + throw error + } + const model = input.model ?? agent.model ?? (yield* lastModel(input.sessionID)) + const userMsg: MessageV2.User = { + id: input.messageID ?? MessageID.ascending(), + sessionID: input.sessionID, + time: { created: Date.now() }, + role: "user", + agent: input.agent, + model: { providerID: model.providerID, modelID: model.modelID }, + } + yield* sessions.updateMessage(userMsg) + const userPart: MessageV2.Part = { + type: "text", + id: PartID.ascending(), + messageID: userMsg.id, + sessionID: input.sessionID, + text: "The following tool was executed by the user", + synthetic: true, + } + yield* sessions.updatePart(userPart) + + const msg: MessageV2.Assistant = { + id: MessageID.ascending(), + sessionID: input.sessionID, + parentID: userMsg.id, + mode: input.agent, + agent: input.agent, + cost: 0, + path: { cwd: ctx.directory, root: ctx.worktree }, + time: { created: Date.now() }, + role: "assistant", + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + modelID: model.modelID, + providerID: model.providerID, + } + yield* sessions.updateMessage(msg) + const part: MessageV2.ToolPart = { + type: "tool", + id: PartID.ascending(), + messageID: msg.id, + sessionID: input.sessionID, + tool: "bash", + callID: ulid(), + state: { + status: "running", + time: { start: Date.now() }, + input: { command: input.command }, + }, + } + yield* sessions.updatePart(part) + + const sh = Shell.preferred() + const shellName = ( + process.platform === "win32" ? path.win32.basename(sh, ".exe") : path.basename(sh) + ).toLowerCase() + const invocations: Record = { + nu: { args: ["-c", input.command] }, + fish: { args: ["-c", input.command] }, + zsh: { + args: [ + "-l", + "-c", + ` + __oc_cwd=$PWD + [[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true + [[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true + cd "$__oc_cwd" + eval ${JSON.stringify(input.command)} + `, + ], + }, + bash: { + args: [ + "-l", + "-c", + ` + __oc_cwd=$PWD + shopt -s expand_aliases + [[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true + cd "$__oc_cwd" + eval ${JSON.stringify(input.command)} + `, + ], + }, + cmd: { args: ["/c", input.command] }, + powershell: { args: ["-NoProfile", "-Command", input.command] }, + pwsh: { args: ["-NoProfile", "-Command", input.command] }, + "": { args: ["-c", input.command] }, + } + + const args = (invocations[shellName] ?? invocations[""]).args + const cwd = ctx.directory + const shellEnv = yield* plugin.trigger( + "shell.env", + { cwd, sessionID: input.sessionID, callID: part.callID }, + { env: {} }, + ) + + const cmd = ChildProcess.make(sh, args, { + cwd, + extendEnv: true, + env: { ...shellEnv.env, TERM: "dumb" }, + stdin: "ignore", + forceKillAfter: "3 seconds", }) - const shellImpl = Effect.fn("SessionPrompt.shellImpl")(function* (input: ShellInput) { - const ctx = yield* InstanceState.context - const run = yield* runner() - const session = yield* sessions.get(input.sessionID) - if (session.revert) { - yield* revert.cleanup(session) - } - const agent = yield* agents.get(input.agent) - if (!agent) { - const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) - const hint = available.length ? ` Available agents: ${available.join(", ")}` : "" - const error = new NamedError.Unknown({ message: `Agent not found: "${input.agent}".${hint}` }) - yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() }) - throw error - } - const model = input.model ?? agent.model ?? (yield* lastModel(input.sessionID)) - const userMsg: MessageV2.User = { - id: input.messageID ?? MessageID.ascending(), - sessionID: input.sessionID, - time: { created: Date.now() }, - role: "user", - agent: input.agent, - model: { providerID: model.providerID, modelID: model.modelID }, - } - yield* sessions.updateMessage(userMsg) - const userPart: MessageV2.Part = { - type: "text", - id: PartID.ascending(), - messageID: userMsg.id, - sessionID: input.sessionID, - text: "The following tool was executed by the user", - synthetic: true, - } - yield* sessions.updatePart(userPart) + let output = "" + let aborted = false - const msg: MessageV2.Assistant = { - id: MessageID.ascending(), - sessionID: input.sessionID, - parentID: userMsg.id, - mode: input.agent, - agent: input.agent, - cost: 0, - path: { cwd: ctx.directory, root: ctx.worktree }, - time: { created: Date.now() }, - role: "assistant", - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - modelID: model.modelID, - providerID: model.providerID, - } - yield* sessions.updateMessage(msg) - const part: MessageV2.ToolPart = { - type: "tool", - id: PartID.ascending(), - messageID: msg.id, - sessionID: input.sessionID, - tool: "bash", - callID: ulid(), - state: { - status: "running", - time: { start: Date.now() }, - input: { command: input.command }, - }, - } - yield* sessions.updatePart(part) - - const sh = Shell.preferred() - const shellName = ( - process.platform === "win32" ? path.win32.basename(sh, ".exe") : path.basename(sh) - ).toLowerCase() - const invocations: Record = { - nu: { args: ["-c", input.command] }, - fish: { args: ["-c", input.command] }, - zsh: { - args: [ - "-l", - "-c", - ` - __oc_cwd=$PWD - [[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true - [[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true - cd "$__oc_cwd" - eval ${JSON.stringify(input.command)} - `, - ], - }, - bash: { - args: [ - "-l", - "-c", - ` - __oc_cwd=$PWD - shopt -s expand_aliases - [[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true - cd "$__oc_cwd" - eval ${JSON.stringify(input.command)} - `, - ], - }, - cmd: { args: ["/c", input.command] }, - powershell: { args: ["-NoProfile", "-Command", input.command] }, - pwsh: { args: ["-NoProfile", "-Command", input.command] }, - "": { args: ["-c", input.command] }, - } - - const args = (invocations[shellName] ?? invocations[""]).args - const cwd = ctx.directory - const shellEnv = yield* plugin.trigger( - "shell.env", - { cwd, sessionID: input.sessionID, callID: part.callID }, - { env: {} }, - ) - - const cmd = ChildProcess.make(sh, args, { - cwd, - extendEnv: true, - env: { ...shellEnv.env, TERM: "dumb" }, - stdin: "ignore", - forceKillAfter: "3 seconds", - }) - - let output = "" - let aborted = false - - const finish = Effect.uninterruptible( - Effect.gen(function* () { - if (aborted) { - output += "\n\n" + ["", "User aborted the command", ""].join("\n") - } - if (!msg.time.completed) { - msg.time.completed = Date.now() - yield* sessions.updateMessage(msg) + const finish = Effect.uninterruptible( + Effect.gen(function* () { + if (aborted) { + output += "\n\n" + ["", "User aborted the command", ""].join("\n") + } + if (!msg.time.completed) { + msg.time.completed = Date.now() + yield* sessions.updateMessage(msg) + } + if (part.state.status === "running") { + part.state = { + status: "completed", + time: { ...part.state.time, end: Date.now() }, + input: part.state.input, + title: "", + metadata: { output, description: "" }, + output, } + yield* sessions.updatePart(part) + } + }), + ) + + const exit = yield* Effect.gen(function* () { + const handle = yield* spawner.spawn(cmd) + yield* Stream.runForEach(Stream.decodeText(handle.all), (chunk) => + Effect.sync(() => { + output += chunk if (part.state.status === "running") { - part.state = { - status: "completed", - time: { ...part.state.time, end: Date.now() }, - input: part.state.input, - title: "", - metadata: { output, description: "" }, - output, - } - yield* sessions.updatePart(part) + part.state.metadata = { output, description: "" } + void run.fork(sessions.updatePart(part)) } }), ) + yield* handle.exitCode + }).pipe( + Effect.scoped, + Effect.onInterrupt(() => + Effect.sync(() => { + aborted = true + }), + ), + Effect.orDie, + Effect.ensuring(finish), + Effect.exit, + ) - const exit = yield* Effect.gen(function* () { - const handle = yield* spawner.spawn(cmd) - yield* Stream.runForEach(Stream.decodeText(handle.all), (chunk) => - Effect.sync(() => { - output += chunk - if (part.state.status === "running") { - part.state.metadata = { output, description: "" } - void run.fork(sessions.updatePart(part)) - } - }), - ) - yield* handle.exitCode - }).pipe( - Effect.scoped, - Effect.onInterrupt(() => - Effect.sync(() => { - aborted = true - }), - ), - Effect.orDie, - Effect.ensuring(finish), - Effect.exit, - ) - - if (Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)) { - return yield* Effect.failCause(exit.cause) - } - - return { info: msg, parts: [part] } - }) - - const getModel = Effect.fn("SessionPrompt.getModel")(function* ( - providerID: ProviderID, - modelID: ModelID, - sessionID: SessionID, - ) { - const exit = yield* provider.getModel(providerID, modelID).pipe(Effect.exit) - if (Exit.isSuccess(exit)) return exit.value - const err = Cause.squash(exit.cause) - if (Provider.ModelNotFoundError.isInstance(err)) { - const hint = err.data.suggestions?.length ? ` Did you mean: ${err.data.suggestions.join(", ")}?` : "" - yield* bus.publish(Session.Event.Error, { - sessionID, - error: new NamedError.Unknown({ - message: `Model not found: ${err.data.providerID}/${err.data.modelID}.${hint}`, - }).toObject(), - }) - } + if (Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)) { return yield* Effect.failCause(exit.cause) - }) + } - const lastModel = Effect.fnUntraced(function* (sessionID: SessionID) { - const match = yield* sessions.findMessage(sessionID, (m) => m.info.role === "user" && !!m.info.model) - if (Option.isSome(match) && match.value.info.role === "user") return match.value.info.model - return yield* provider.defaultModel() - }) + return { info: msg, parts: [part] } + }) - const createUserMessage = Effect.fn("SessionPrompt.createUserMessage")(function* (input: PromptInput) { - const agentName = input.agent || (yield* agents.defaultAgent()) - const ag = yield* agents.get(agentName) - if (!ag) { - const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) - const hint = available.length ? ` Available agents: ${available.join(", ")}` : "" - const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` }) - yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() }) - throw error - } - - const model = input.model ?? ag.model ?? (yield* lastModel(input.sessionID)) - const same = ag.model && model.providerID === ag.model.providerID && model.modelID === ag.model.modelID - const full = - !input.variant && ag.variant && same - ? yield* provider.getModel(model.providerID, model.modelID).pipe(Effect.catchDefect(() => Effect.void)) - : undefined - const variant = input.variant ?? (ag.variant && full?.variants?.[ag.variant] ? ag.variant : undefined) - - const info: MessageV2.User = { - id: input.messageID ?? MessageID.ascending(), - role: "user", - sessionID: input.sessionID, - time: { created: Date.now() }, - tools: input.tools, - agent: ag.name, - model: { - providerID: model.providerID, - modelID: model.modelID, - variant, - }, - system: input.system, - format: input.format, - } - - yield* Effect.addFinalizer(() => instruction.clear(info.id)) - - type Draft = T extends MessageV2.Part ? Omit & { id?: string } : never - const assign = (part: Draft): MessageV2.Part => ({ - ...part, - id: part.id ? PartID.make(part.id) : PartID.ascending(), + const getModel = Effect.fn("SessionPrompt.getModel")(function* ( + providerID: ProviderID, + modelID: ModelID, + sessionID: SessionID, + ) { + const exit = yield* provider.getModel(providerID, modelID).pipe(Effect.exit) + if (Exit.isSuccess(exit)) return exit.value + const err = Cause.squash(exit.cause) + if (Provider.ModelNotFoundError.isInstance(err)) { + const hint = err.data.suggestions?.length ? ` Did you mean: ${err.data.suggestions.join(", ")}?` : "" + yield* bus.publish(Session.Event.Error, { + sessionID, + error: new NamedError.Unknown({ + message: `Model not found: ${err.data.providerID}/${err.data.modelID}.${hint}`, + }).toObject(), }) + } + return yield* Effect.failCause(exit.cause) + }) - const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect[]> = Effect.fn( - "SessionPrompt.resolveUserPart", - )(function* (part) { - if (part.type === "file") { - if (part.source?.type === "resource") { - const { clientName, uri } = part.source - log.info("mcp resource", { clientName, uri, mime: part.mime }) - const pieces: Draft[] = [ - { - messageID: info.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text: `Reading MCP resource: ${part.filename} (${uri})`, - }, - ] - const exit = yield* mcp.readResource(clientName, uri).pipe(Effect.exit) - if (Exit.isSuccess(exit)) { - const content = exit.value - if (!content) throw new Error(`Resource not found: ${clientName}/${uri}`) - const items = Array.isArray(content.contents) ? content.contents : [content.contents] - for (const c of items) { - if ("text" in c && c.text) { - pieces.push({ - messageID: info.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text: c.text, - }) - } else if ("blob" in c && c.blob) { - const mime = "mimeType" in c ? c.mimeType : part.mime - pieces.push({ - messageID: info.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text: `[Binary content: ${mime}]`, - }) - } + const lastModel = Effect.fnUntraced(function* (sessionID: SessionID) { + const match = yield* sessions.findMessage(sessionID, (m) => m.info.role === "user" && !!m.info.model) + if (Option.isSome(match) && match.value.info.role === "user") return match.value.info.model + return yield* provider.defaultModel() + }) + + const createUserMessage = Effect.fn("SessionPrompt.createUserMessage")(function* (input: PromptInput) { + const agentName = input.agent || (yield* agents.defaultAgent()) + const ag = yield* agents.get(agentName) + if (!ag) { + const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) + const hint = available.length ? ` Available agents: ${available.join(", ")}` : "" + const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` }) + yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() }) + throw error + } + + const model = input.model ?? ag.model ?? (yield* lastModel(input.sessionID)) + const same = ag.model && model.providerID === ag.model.providerID && model.modelID === ag.model.modelID + const full = + !input.variant && ag.variant && same + ? yield* provider.getModel(model.providerID, model.modelID).pipe(Effect.catchDefect(() => Effect.void)) + : undefined + const variant = input.variant ?? (ag.variant && full?.variants?.[ag.variant] ? ag.variant : undefined) + + const info: MessageV2.User = { + id: input.messageID ?? MessageID.ascending(), + role: "user", + sessionID: input.sessionID, + time: { created: Date.now() }, + tools: input.tools, + agent: ag.name, + model: { + providerID: model.providerID, + modelID: model.modelID, + variant, + }, + system: input.system, + format: input.format, + } + + yield* Effect.addFinalizer(() => instruction.clear(info.id)) + + type Draft = T extends MessageV2.Part ? Omit & { id?: string } : never + const assign = (part: Draft): MessageV2.Part => ({ + ...part, + id: part.id ? PartID.make(part.id) : PartID.ascending(), + }) + + const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect[]> = Effect.fn( + "SessionPrompt.resolveUserPart", + )(function* (part) { + if (part.type === "file") { + if (part.source?.type === "resource") { + const { clientName, uri } = part.source + log.info("mcp resource", { clientName, uri, mime: part.mime }) + const pieces: Draft[] = [ + { + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: `Reading MCP resource: ${part.filename} (${uri})`, + }, + ] + const exit = yield* mcp.readResource(clientName, uri).pipe(Effect.exit) + if (Exit.isSuccess(exit)) { + const content = exit.value + if (!content) throw new Error(`Resource not found: ${clientName}/${uri}`) + const items = Array.isArray(content.contents) ? content.contents : [content.contents] + for (const c of items) { + if ("text" in c && c.text) { + pieces.push({ + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: c.text, + }) + } else if ("blob" in c && c.blob) { + const mime = "mimeType" in c ? c.mimeType : part.mime + pieces.push({ + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: `[Binary content: ${mime}]`, + }) } - pieces.push({ ...part, messageID: info.id, sessionID: input.sessionID }) - } else { - const error = Cause.squash(exit.cause) - log.error("failed to read MCP resource", { error, clientName, uri }) - const message = error instanceof Error ? error.message : String(error) - pieces.push({ - messageID: info.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text: `Failed to read MCP resource ${part.filename}: ${message}`, - }) } - return pieces + pieces.push({ ...part, messageID: info.id, sessionID: input.sessionID }) + } else { + const error = Cause.squash(exit.cause) + log.error("failed to read MCP resource", { error, clientName, uri }) + const message = error instanceof Error ? error.message : String(error) + pieces.push({ + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: `Failed to read MCP resource ${part.filename}: ${message}`, + }) } - const url = new URL(part.url) - switch (url.protocol) { - case "data:": - if (part.mime === "text/plain") { - return [ - { - messageID: info.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: part.filename })}`, - }, - { - messageID: info.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text: decodeDataUrl(part.url), - }, - { ...part, messageID: info.id, sessionID: input.sessionID }, - ] - } - break - case "file:": { - log.info("file", { mime: part.mime }) - const filepath = fileURLToPath(part.url) - if (yield* fsys.isDir(filepath)) part.mime = "application/x-directory" - - const { read } = yield* registry.named() - const execRead = (args: Parameters[0], extra?: Tool.Context["extra"]) => { - const controller = new AbortController() - return read - .execute(args, { - sessionID: input.sessionID, - abort: controller.signal, - agent: input.agent!, - messageID: info.id, - extra: { bypassCwdCheck: true, ...extra }, - messages: [], - metadata: () => Effect.void, - ask: () => Effect.void, - }) - .pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort()))) - } - - if (part.mime === "text/plain") { - let offset: number | undefined - let limit: number | undefined - const range = { start: url.searchParams.get("start"), end: url.searchParams.get("end") } - if (range.start != null) { - const filePathURI = part.url.split("?")[0] - let start = parseInt(range.start) - let end = range.end ? parseInt(range.end) : undefined - if (start === end) { - const symbols = yield* lsp - .documentSymbol(filePathURI) - .pipe(Effect.catch(() => Effect.succeed([]))) - for (const symbol of symbols) { - let r: LSP.Range | undefined - if ("range" in symbol) r = symbol.range - else if ("location" in symbol) r = symbol.location.range - if (r?.start?.line && r?.start?.line === start) { - start = r.start.line - end = r?.end?.line ?? start - break - } - } - } - offset = Math.max(start, 1) - if (end) limit = end - (offset - 1) - } - const args = { filePath: filepath, offset, limit } - const pieces: Draft[] = [ - { - messageID: info.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text: `Called the Read tool with the following input: ${JSON.stringify(args)}`, - }, - ] - const exit = yield* provider.getModel(info.model.providerID, info.model.modelID).pipe( - Effect.flatMap((mdl) => execRead(args, { model: mdl })), - Effect.exit, - ) - if (Exit.isSuccess(exit)) { - const result = exit.value - pieces.push({ - messageID: info.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text: result.output, - }) - if (result.attachments?.length) { - pieces.push( - ...result.attachments.map((a) => ({ - ...a, - synthetic: true, - filename: a.filename ?? part.filename, - messageID: info.id, - sessionID: input.sessionID, - })), - ) - } else { - pieces.push({ ...part, messageID: info.id, sessionID: input.sessionID }) - } - } else { - const error = Cause.squash(exit.cause) - log.error("failed to read file", { error }) - const message = error instanceof Error ? error.message : String(error) - yield* bus.publish(Session.Event.Error, { - sessionID: input.sessionID, - error: new NamedError.Unknown({ message }).toObject(), - }) - pieces.push({ - messageID: info.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text: `Read tool failed to read ${filepath} with the following error: ${message}`, - }) - } - return pieces - } - - if (part.mime === "application/x-directory") { - const args = { filePath: filepath } - const exit = yield* execRead(args).pipe(Effect.exit) - if (Exit.isFailure(exit)) { - const error = Cause.squash(exit.cause) - log.error("failed to read directory", { error }) - const message = error instanceof Error ? error.message : String(error) - yield* bus.publish(Session.Event.Error, { - sessionID: input.sessionID, - error: new NamedError.Unknown({ message }).toObject(), - }) - return [ - { - messageID: info.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text: `Read tool failed to read ${filepath} with the following error: ${message}`, - }, - ] - } - return [ - { - messageID: info.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text: `Called the Read tool with the following input: ${JSON.stringify(args)}`, - }, - { - messageID: info.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text: exit.value.output, - }, - { ...part, messageID: info.id, sessionID: input.sessionID }, - ] - } - - yield* filetime.read(input.sessionID, filepath) + return pieces + } + const url = new URL(part.url) + switch (url.protocol) { + case "data:": + if (part.mime === "text/plain") { return [ { messageID: info.id, sessionID: input.sessionID, type: "text", synthetic: true, - text: `Called the Read tool with the following input: {"filePath":"${filepath}"}`, + text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: part.filename })}`, }, { - id: part.id, messageID: info.id, sessionID: input.sessionID, - type: "file", - url: - `data:${part.mime};base64,` + - Buffer.from(yield* fsys.readFile(filepath).pipe(Effect.catch(Effect.die))).toString("base64"), - mime: part.mime, - filename: part.filename!, - source: part.source, + type: "text", + synthetic: true, + text: decodeDataUrl(part.url), }, + { ...part, messageID: info.id, sessionID: input.sessionID }, ] } + break + case "file:": { + log.info("file", { mime: part.mime }) + const filepath = fileURLToPath(part.url) + if (yield* fsys.isDir(filepath)) part.mime = "application/x-directory" + + const { read } = yield* registry.named() + const execRead = (args: Parameters[0], extra?: Tool.Context["extra"]) => { + const controller = new AbortController() + return read + .execute(args, { + sessionID: input.sessionID, + abort: controller.signal, + agent: input.agent!, + messageID: info.id, + extra: { bypassCwdCheck: true, ...extra }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }) + .pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort()))) + } + + if (part.mime === "text/plain") { + let offset: number | undefined + let limit: number | undefined + const range = { start: url.searchParams.get("start"), end: url.searchParams.get("end") } + if (range.start != null) { + const filePathURI = part.url.split("?")[0] + let start = parseInt(range.start) + let end = range.end ? parseInt(range.end) : undefined + if (start === end) { + const symbols = yield* lsp + .documentSymbol(filePathURI) + .pipe(Effect.catch(() => Effect.succeed([]))) + for (const symbol of symbols) { + let r: LSP.Range | undefined + if ("range" in symbol) r = symbol.range + else if ("location" in symbol) r = symbol.location.range + if (r?.start?.line && r?.start?.line === start) { + start = r.start.line + end = r?.end?.line ?? start + break + } + } + } + offset = Math.max(start, 1) + if (end) limit = end - (offset - 1) + } + const args = { filePath: filepath, offset, limit } + const pieces: Draft[] = [ + { + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: `Called the Read tool with the following input: ${JSON.stringify(args)}`, + }, + ] + const exit = yield* provider.getModel(info.model.providerID, info.model.modelID).pipe( + Effect.flatMap((mdl) => execRead(args, { model: mdl })), + Effect.exit, + ) + if (Exit.isSuccess(exit)) { + const result = exit.value + pieces.push({ + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: result.output, + }) + if (result.attachments?.length) { + pieces.push( + ...result.attachments.map((a) => ({ + ...a, + synthetic: true, + filename: a.filename ?? part.filename, + messageID: info.id, + sessionID: input.sessionID, + })), + ) + } else { + pieces.push({ ...part, messageID: info.id, sessionID: input.sessionID }) + } + } else { + const error = Cause.squash(exit.cause) + log.error("failed to read file", { error }) + const message = error instanceof Error ? error.message : String(error) + yield* bus.publish(Session.Event.Error, { + sessionID: input.sessionID, + error: new NamedError.Unknown({ message }).toObject(), + }) + pieces.push({ + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: `Read tool failed to read ${filepath} with the following error: ${message}`, + }) + } + return pieces + } + + if (part.mime === "application/x-directory") { + const args = { filePath: filepath } + const exit = yield* execRead(args).pipe(Effect.exit) + if (Exit.isFailure(exit)) { + const error = Cause.squash(exit.cause) + log.error("failed to read directory", { error }) + const message = error instanceof Error ? error.message : String(error) + yield* bus.publish(Session.Event.Error, { + sessionID: input.sessionID, + error: new NamedError.Unknown({ message }).toObject(), + }) + return [ + { + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: `Read tool failed to read ${filepath} with the following error: ${message}`, + }, + ] + } + return [ + { + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: `Called the Read tool with the following input: ${JSON.stringify(args)}`, + }, + { + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: exit.value.output, + }, + { ...part, messageID: info.id, sessionID: input.sessionID }, + ] + } + + yield* filetime.read(input.sessionID, filepath) + return [ + { + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: `Called the Read tool with the following input: {"filePath":"${filepath}"}`, + }, + { + id: part.id, + messageID: info.id, + sessionID: input.sessionID, + type: "file", + url: + `data:${part.mime};base64,` + + Buffer.from(yield* fsys.readFile(filepath).pipe(Effect.catch(Effect.die))).toString("base64"), + mime: part.mime, + filename: part.filename!, + source: part.source, + }, + ] } } - - if (part.type === "agent") { - const perm = Permission.evaluate("task", part.name, ag.permission) - const hint = perm.action === "deny" ? " . Invoked by user; guaranteed to exist." : "" - return [ - { ...part, messageID: info.id, sessionID: input.sessionID }, - { - messageID: info.id, - sessionID: input.sessionID, - type: "text", - synthetic: true, - text: - " Use the above message and context to generate a prompt and call the task tool with subagent: " + - part.name + - hint, - }, - ] - } - - return [{ ...part, messageID: info.id, sessionID: input.sessionID }] - }) - - const parts = yield* Effect.forEach(input.parts, resolvePart, { concurrency: "unbounded" }).pipe( - Effect.map((x) => x.flat().map(assign)), - ) - - yield* plugin.trigger( - "chat.message", - { - sessionID: input.sessionID, - agent: input.agent, - model: input.model, - messageID: input.messageID, - variant: input.variant, - }, - { message: info, parts }, - ) - - const parsed = MessageV2.Info.safeParse(info) - if (!parsed.success) { - log.error("invalid user message before save", { - sessionID: input.sessionID, - messageID: info.id, - agent: info.agent, - model: info.model, - issues: parsed.error.issues, - }) } - parts.forEach((part, index) => { - const p = MessageV2.Part.safeParse(part) - if (p.success) return - log.error("invalid user part before save", { - sessionID: input.sessionID, - messageID: info.id, - partID: part.id, - partType: part.type, - index, - issues: p.error.issues, - part, - }) - }) - yield* sessions.updateMessage(info) - for (const part of parts) yield* sessions.updatePart(part) + if (part.type === "agent") { + const perm = Permission.evaluate("task", part.name, ag.permission) + const hint = perm.action === "deny" ? " . Invoked by user; guaranteed to exist." : "" + return [ + { ...part, messageID: info.id, sessionID: input.sessionID }, + { + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: + " Use the above message and context to generate a prompt and call the task tool with subagent: " + + part.name + + hint, + }, + ] + } - return { info, parts } - }, Effect.scoped) - - const prompt: (input: PromptInput) => Effect.Effect = Effect.fn("SessionPrompt.prompt")( - function* (input: PromptInput) { - const session = yield* sessions.get(input.sessionID) - yield* revert.cleanup(session) - const message = yield* createUserMessage(input) - yield* sessions.touch(input.sessionID) - - const permissions: Permission.Ruleset = [] - for (const [t, enabled] of Object.entries(input.tools ?? {})) { - permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" }) - } - if (permissions.length > 0) { - session.permission = permissions - yield* sessions.setPermission({ sessionID: session.id, permission: permissions }) - } - - if (input.noReply === true) return message - return yield* loop({ sessionID: input.sessionID }) - }, - ) - - const lastAssistant = Effect.fnUntraced(function* (sessionID: SessionID) { - const match = yield* sessions.findMessage(sessionID, (m) => m.info.role !== "user") - if (Option.isSome(match)) return match.value - const msgs = yield* sessions.messages({ sessionID, limit: 1 }) - if (msgs.length > 0) return msgs[0] - throw new Error("Impossible") + return [{ ...part, messageID: info.id, sessionID: input.sessionID }] }) - const runLoop: (sessionID: SessionID) => Effect.Effect = Effect.fn("SessionPrompt.run")( - function* (sessionID: SessionID) { - const ctx = yield* InstanceState.context - const slog = elog.with({ sessionID }) - let structured: unknown | undefined - let step = 0 - const session = yield* sessions.get(sessionID) + const parts = yield* Effect.forEach(input.parts, resolvePart, { concurrency: "unbounded" }).pipe( + Effect.map((x) => x.flat().map(assign)), + ) - while (true) { - yield* status.set(sessionID, { type: "busy" }) - yield* slog.info("loop", { step }) + yield* plugin.trigger( + "chat.message", + { + sessionID: input.sessionID, + agent: input.agent, + model: input.model, + messageID: input.messageID, + variant: input.variant, + }, + { message: info, parts }, + ) - let msgs = yield* MessageV2.filterCompactedEffect(sessionID) + const parsed = MessageV2.Info.safeParse(info) + if (!parsed.success) { + log.error("invalid user message before save", { + sessionID: input.sessionID, + messageID: info.id, + agent: info.agent, + model: info.model, + issues: parsed.error.issues, + }) + } + parts.forEach((part, index) => { + const p = MessageV2.Part.safeParse(part) + if (p.success) return + log.error("invalid user part before save", { + sessionID: input.sessionID, + messageID: info.id, + partID: part.id, + partType: part.type, + index, + issues: p.error.issues, + part, + }) + }) - let lastUser: MessageV2.User | undefined - let lastAssistant: MessageV2.Assistant | undefined - let lastFinished: MessageV2.Assistant | undefined - let tasks: (MessageV2.CompactionPart | MessageV2.SubtaskPart)[] = [] - for (let i = msgs.length - 1; i >= 0; i--) { - const msg = msgs[i] - if (!lastUser && msg.info.role === "user") lastUser = msg.info - if (!lastAssistant && msg.info.role === "assistant") lastAssistant = msg.info - if (!lastFinished && msg.info.role === "assistant" && msg.info.finish) lastFinished = msg.info - if (lastUser && lastFinished) break - const task = msg.parts.filter((part) => part.type === "compaction" || part.type === "subtask") - if (task && !lastFinished) tasks.push(...task) - } + yield* sessions.updateMessage(info) + for (const part of parts) yield* sessions.updatePart(part) - if (!lastUser) throw new Error("No user message found in stream. This should never happen.") + return { info, parts } + }, Effect.scoped) - const lastAssistantMsg = msgs.findLast( - (msg) => msg.info.role === "assistant" && msg.info.id === lastAssistant?.id, - ) - // Some providers return "stop" even when the assistant message contains tool calls. - // Keep the loop running so tool results can be sent back to the model. - // Skip provider-executed tool parts — those were fully handled within the - // provider's stream (e.g. DWS Agent Platform) and don't need a re-loop. - const hasToolCalls = - lastAssistantMsg?.parts.some((part) => part.type === "tool" && !part.metadata?.providerExecuted) ?? false + const prompt: (input: PromptInput) => Effect.Effect = Effect.fn("SessionPrompt.prompt")( + function* (input: PromptInput) { + const session = yield* sessions.get(input.sessionID) + yield* revert.cleanup(session) + const message = yield* createUserMessage(input) + yield* sessions.touch(input.sessionID) - if ( - lastAssistant?.finish && - !["tool-calls"].includes(lastAssistant.finish) && - !hasToolCalls && - lastUser.id < lastAssistant.id - ) { - yield* slog.info("exiting loop") - break - } + const permissions: Permission.Ruleset = [] + for (const [t, enabled] of Object.entries(input.tools ?? {})) { + permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" }) + } + if (permissions.length > 0) { + session.permission = permissions + yield* sessions.setPermission({ sessionID: session.id, permission: permissions }) + } - step++ - if (step === 1) - yield* title({ - session, - modelID: lastUser.model.modelID, - providerID: lastUser.model.providerID, - history: msgs, - }).pipe(Effect.ignore, Effect.forkIn(scope)) + if (input.noReply === true) return message + return yield* loop({ sessionID: input.sessionID }) + }, + ) - const model = yield* getModel(lastUser.model.providerID, lastUser.model.modelID, sessionID) - const task = tasks.pop() + const lastAssistant = Effect.fnUntraced(function* (sessionID: SessionID) { + const match = yield* sessions.findMessage(sessionID, (m) => m.info.role !== "user") + if (Option.isSome(match)) return match.value + const msgs = yield* sessions.messages({ sessionID, limit: 1 }) + if (msgs.length > 0) return msgs[0] + throw new Error("Impossible") + }) - if (task?.type === "subtask") { - yield* handleSubtask({ task, model, lastUser, sessionID, session, msgs }) - continue - } + const runLoop: (sessionID: SessionID) => Effect.Effect = Effect.fn("SessionPrompt.run")( + function* (sessionID: SessionID) { + const ctx = yield* InstanceState.context + const slog = elog.with({ sessionID }) + let structured: unknown | undefined + let step = 0 + const session = yield* sessions.get(sessionID) - if (task?.type === "compaction") { - const result = yield* compaction.process({ - messages: msgs, - parentID: lastUser.id, - sessionID, - auto: task.auto, - overflow: task.overflow, - }) - if (result === "stop") break - continue - } + while (true) { + yield* status.set(sessionID, { type: "busy" }) + yield* slog.info("loop", { step }) - if ( - lastFinished && - lastFinished.summary !== true && - (yield* compaction.isOverflow({ tokens: lastFinished.tokens, model })) - ) { - yield* compaction.create({ sessionID, agent: lastUser.agent, model: lastUser.model, auto: true }) - continue - } + let msgs = yield* MessageV2.filterCompactedEffect(sessionID) - const agent = yield* agents.get(lastUser.agent) - if (!agent) { - const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) - const hint = available.length ? ` Available agents: ${available.join(", ")}` : "" - const error = new NamedError.Unknown({ message: `Agent not found: "${lastUser.agent}".${hint}` }) - yield* bus.publish(Session.Event.Error, { sessionID, error: error.toObject() }) - throw error - } - const maxSteps = agent.steps ?? Infinity - const isLastStep = step >= maxSteps - msgs = yield* insertReminders({ messages: msgs, agent, session }) + let lastUser: MessageV2.User | undefined + let lastAssistant: MessageV2.Assistant | undefined + let lastFinished: MessageV2.Assistant | undefined + let tasks: (MessageV2.CompactionPart | MessageV2.SubtaskPart)[] = [] + for (let i = msgs.length - 1; i >= 0; i--) { + const msg = msgs[i] + if (!lastUser && msg.info.role === "user") lastUser = msg.info + if (!lastAssistant && msg.info.role === "assistant") lastAssistant = msg.info + if (!lastFinished && msg.info.role === "assistant" && msg.info.finish) lastFinished = msg.info + if (lastUser && lastFinished) break + const task = msg.parts.filter((part) => part.type === "compaction" || part.type === "subtask") + if (task && !lastFinished) tasks.push(...task) + } - const msg: MessageV2.Assistant = { - id: MessageID.ascending(), - parentID: lastUser.id, - role: "assistant", - mode: agent.name, - agent: agent.name, - variant: lastUser.model.variant, - path: { cwd: ctx.directory, root: ctx.worktree }, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - modelID: model.id, - providerID: model.providerID, - time: { created: Date.now() }, - sessionID, - } - yield* sessions.updateMessage(msg) - const handle = yield* processor.create({ - assistantMessage: msg, - sessionID, - model, - }) + if (!lastUser) throw new Error("No user message found in stream. This should never happen.") - const outcome: "break" | "continue" = yield* Effect.gen(function* () { - const lastUserMsg = msgs.findLast((m) => m.info.role === "user") - const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false + const lastAssistantMsg = msgs.findLast( + (msg) => msg.info.role === "assistant" && msg.info.id === lastAssistant?.id, + ) + // Some providers return "stop" even when the assistant message contains tool calls. + // Keep the loop running so tool results can be sent back to the model. + // Skip provider-executed tool parts — those were fully handled within the + // provider's stream (e.g. DWS Agent Platform) and don't need a re-loop. + const hasToolCalls = + lastAssistantMsg?.parts.some((part) => part.type === "tool" && !part.metadata?.providerExecuted) ?? false - const tools = yield* resolveTools({ - agent, - session, - model, - tools: lastUser.tools, - processor: handle, - bypassAgentCheck, - messages: msgs, - }) + if ( + lastAssistant?.finish && + !["tool-calls"].includes(lastAssistant.finish) && + !hasToolCalls && + lastUser.id < lastAssistant.id + ) { + yield* slog.info("exiting loop") + break + } - if (lastUser.format?.type === "json_schema") { - tools["StructuredOutput"] = createStructuredOutputTool({ - schema: lastUser.format.schema, - onSuccess(output) { - structured = output - }, - }) - } + step++ + if (step === 1) + yield* title({ + session, + modelID: lastUser.model.modelID, + providerID: lastUser.model.providerID, + history: msgs, + }).pipe(Effect.ignore, Effect.forkIn(scope)) - if (step === 1) - yield* summary - .summarize({ sessionID, messageID: lastUser.id }) - .pipe(Effect.ignore, Effect.forkIn(scope)) + const model = yield* getModel(lastUser.model.providerID, lastUser.model.modelID, sessionID) + const task = tasks.pop() - if (step > 1 && lastFinished) { - for (const m of msgs) { - if (m.info.role !== "user" || m.info.id <= lastFinished.id) continue - for (const p of m.parts) { - if (p.type !== "text" || p.ignored || p.synthetic) continue - if (!p.text.trim()) continue - p.text = [ - "", - "The user sent the following message:", - p.text, - "", - "Please address this message and continue with your tasks.", - "", - ].join("\n") - } - } - } - - yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs }) - - const [skills, env, instructions, modelMsgs] = yield* Effect.all([ - sys.skills(agent), - Effect.sync(() => sys.environment(model)), - instruction.system().pipe(Effect.orDie), - MessageV2.toModelMessagesEffect(msgs, model), - ]) - const system = [...env, ...(skills ? [skills] : []), ...instructions] - const format = lastUser.format ?? { type: "text" as const } - if (format.type === "json_schema") system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT) - const result = yield* handle.process({ - user: lastUser, - agent, - permission: session.permission, - sessionID, - parentSessionID: session.parentID, - system, - messages: [...modelMsgs, ...(isLastStep ? [{ role: "assistant" as const, content: MAX_STEPS }] : [])], - tools, - model, - toolChoice: format.type === "json_schema" ? "required" : undefined, - }) - - if (structured !== undefined) { - handle.message.structured = structured - handle.message.finish = handle.message.finish ?? "stop" - yield* sessions.updateMessage(handle.message) - return "break" as const - } - - const finished = handle.message.finish && !["tool-calls", "unknown"].includes(handle.message.finish) - if (finished && !handle.message.error) { - if (format.type === "json_schema") { - handle.message.error = new MessageV2.StructuredOutputError({ - message: "Model did not produce structured output", - retries: 0, - }).toObject() - yield* sessions.updateMessage(handle.message) - return "break" as const - } - } - - if (result === "stop") return "break" as const - if (result === "compact") { - yield* compaction.create({ - sessionID, - agent: lastUser.agent, - model: lastUser.model, - auto: true, - overflow: !handle.message.finish, - }) - } - return "continue" as const - }).pipe(Effect.ensuring(instruction.clear(handle.message.id))) - if (outcome === "break") break + if (task?.type === "subtask") { + yield* handleSubtask({ task, model, lastUser, sessionID, session, msgs }) continue } - yield* compaction.prune({ sessionID }).pipe(Effect.ignore, Effect.forkIn(scope)) - return yield* lastAssistant(sessionID) - }, - ) - - const loop: (input: z.infer) => Effect.Effect = Effect.fn( - "SessionPrompt.loop", - )(function* (input: z.infer) { - return yield* state.ensureRunning(input.sessionID, lastAssistant(input.sessionID), runLoop(input.sessionID)) - }) - - const shell: (input: ShellInput) => Effect.Effect = Effect.fn("SessionPrompt.shell")( - function* (input: ShellInput) { - return yield* state.startShell(input.sessionID, lastAssistant(input.sessionID), shellImpl(input)) - }, - ) - - const command = Effect.fn("SessionPrompt.command")(function* (input: CommandInput) { - yield* elog.info("command", { sessionID: input.sessionID, command: input.command, agent: input.agent }) - const cmd = yield* commands.get(input.command) - if (!cmd) { - const available = (yield* commands.list()).map((c) => c.name) - const hint = available.length ? ` Available commands: ${available.join(", ")}` : "" - const error = new NamedError.Unknown({ message: `Command not found: "${input.command}".${hint}` }) - yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() }) - throw error - } - const agentName = cmd.agent ?? input.agent ?? (yield* agents.defaultAgent()) - - const raw = input.arguments.match(argsRegex) ?? [] - const args = raw.map((arg) => arg.replace(quoteTrimRegex, "")) - const templateCommand = yield* Effect.promise(async () => cmd.template) - - const placeholders = templateCommand.match(placeholderRegex) ?? [] - let last = 0 - for (const item of placeholders) { - const value = Number(item.slice(1)) - if (value > last) last = value - } - - const withArgs = templateCommand.replaceAll(placeholderRegex, (_, index) => { - const position = Number(index) - const argIndex = position - 1 - if (argIndex >= args.length) return "" - if (position === last) return args.slice(argIndex).join(" ") - return args[argIndex] - }) - const usesArgumentsPlaceholder = templateCommand.includes("$ARGUMENTS") - let template = withArgs.replaceAll("$ARGUMENTS", input.arguments) - - if (placeholders.length === 0 && !usesArgumentsPlaceholder && input.arguments.trim()) { - template = template + "\n\n" + input.arguments - } - - const shellMatches = ConfigMarkdown.shell(template) - if (shellMatches.length > 0) { - const sh = Shell.preferred() - const results = yield* Effect.promise(() => - Promise.all( - shellMatches.map(async ([, cmd]) => (await Process.text([cmd], { shell: sh, nothrow: true })).text), - ), - ) - let index = 0 - template = template.replace(bashRegex, () => results[index++]) - } - template = template.trim() - - const taskModel = yield* Effect.gen(function* () { - if (cmd.model) return Provider.parseModel(cmd.model) - if (cmd.agent) { - const cmdAgent = yield* agents.get(cmd.agent) - if (cmdAgent?.model) return cmdAgent.model + if (task?.type === "compaction") { + const result = yield* compaction.process({ + messages: msgs, + parentID: lastUser.id, + sessionID, + auto: task.auto, + overflow: task.overflow, + }) + if (result === "stop") break + continue } - if (input.model) return Provider.parseModel(input.model) - return yield* lastModel(input.sessionID) - }) - yield* getModel(taskModel.providerID, taskModel.modelID, input.sessionID) + if ( + lastFinished && + lastFinished.summary !== true && + (yield* compaction.isOverflow({ tokens: lastFinished.tokens, model })) + ) { + yield* compaction.create({ sessionID, agent: lastUser.agent, model: lastUser.model, auto: true }) + continue + } - const agent = yield* agents.get(agentName) - if (!agent) { - const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) - const hint = available.length ? ` Available agents: ${available.join(", ")}` : "" - const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` }) - yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() }) - throw error + const agent = yield* agents.get(lastUser.agent) + if (!agent) { + const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) + const hint = available.length ? ` Available agents: ${available.join(", ")}` : "" + const error = new NamedError.Unknown({ message: `Agent not found: "${lastUser.agent}".${hint}` }) + yield* bus.publish(Session.Event.Error, { sessionID, error: error.toObject() }) + throw error + } + const maxSteps = agent.steps ?? Infinity + const isLastStep = step >= maxSteps + msgs = yield* insertReminders({ messages: msgs, agent, session }) + + const msg: MessageV2.Assistant = { + id: MessageID.ascending(), + parentID: lastUser.id, + role: "assistant", + mode: agent.name, + agent: agent.name, + variant: lastUser.model.variant, + path: { cwd: ctx.directory, root: ctx.worktree }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + modelID: model.id, + providerID: model.providerID, + time: { created: Date.now() }, + sessionID, + } + yield* sessions.updateMessage(msg) + const handle = yield* processor.create({ + assistantMessage: msg, + sessionID, + model, + }) + + const outcome: "break" | "continue" = yield* Effect.gen(function* () { + const lastUserMsg = msgs.findLast((m) => m.info.role === "user") + const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false + + const tools = yield* resolveTools({ + agent, + session, + model, + tools: lastUser.tools, + processor: handle, + bypassAgentCheck, + messages: msgs, + }) + + if (lastUser.format?.type === "json_schema") { + tools["StructuredOutput"] = createStructuredOutputTool({ + schema: lastUser.format.schema, + onSuccess(output) { + structured = output + }, + }) + } + + if (step === 1) + yield* summary + .summarize({ sessionID, messageID: lastUser.id }) + .pipe(Effect.ignore, Effect.forkIn(scope)) + + if (step > 1 && lastFinished) { + for (const m of msgs) { + if (m.info.role !== "user" || m.info.id <= lastFinished.id) continue + for (const p of m.parts) { + if (p.type !== "text" || p.ignored || p.synthetic) continue + if (!p.text.trim()) continue + p.text = [ + "", + "The user sent the following message:", + p.text, + "", + "Please address this message and continue with your tasks.", + "", + ].join("\n") + } + } + } + + yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs }) + + const [skills, env, instructions, modelMsgs] = yield* Effect.all([ + sys.skills(agent), + Effect.sync(() => sys.environment(model)), + instruction.system().pipe(Effect.orDie), + MessageV2.toModelMessagesEffect(msgs, model), + ]) + const system = [...env, ...(skills ? [skills] : []), ...instructions] + const format = lastUser.format ?? { type: "text" as const } + if (format.type === "json_schema") system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT) + const result = yield* handle.process({ + user: lastUser, + agent, + permission: session.permission, + sessionID, + parentSessionID: session.parentID, + system, + messages: [...modelMsgs, ...(isLastStep ? [{ role: "assistant" as const, content: MAX_STEPS }] : [])], + tools, + model, + toolChoice: format.type === "json_schema" ? "required" : undefined, + }) + + if (structured !== undefined) { + handle.message.structured = structured + handle.message.finish = handle.message.finish ?? "stop" + yield* sessions.updateMessage(handle.message) + return "break" as const + } + + const finished = handle.message.finish && !["tool-calls", "unknown"].includes(handle.message.finish) + if (finished && !handle.message.error) { + if (format.type === "json_schema") { + handle.message.error = new MessageV2.StructuredOutputError({ + message: "Model did not produce structured output", + retries: 0, + }).toObject() + yield* sessions.updateMessage(handle.message) + return "break" as const + } + } + + if (result === "stop") return "break" as const + if (result === "compact") { + yield* compaction.create({ + sessionID, + agent: lastUser.agent, + model: lastUser.model, + auto: true, + overflow: !handle.message.finish, + }) + } + return "continue" as const + }).pipe(Effect.ensuring(instruction.clear(handle.message.id))) + if (outcome === "break") break + continue } - const templateParts = yield* resolvePromptParts(template) - const isSubtask = (agent.mode === "subagent" && cmd.subtask !== false) || cmd.subtask === true - const parts = isSubtask - ? [ - { - type: "subtask" as const, - agent: agent.name, - description: cmd.description ?? "", - command: input.command, - model: { providerID: taskModel.providerID, modelID: taskModel.modelID }, - prompt: templateParts.find((y) => y.type === "text")?.text ?? "", - }, - ] - : [...templateParts, ...(input.parts ?? [])] + yield* compaction.prune({ sessionID }).pipe(Effect.ignore, Effect.forkIn(scope)) + return yield* lastAssistant(sessionID) + }, + ) - const userAgent = isSubtask ? (input.agent ?? (yield* agents.defaultAgent())) : agentName - const userModel = isSubtask - ? input.model - ? Provider.parseModel(input.model) - : yield* lastModel(input.sessionID) - : taskModel + const loop: (input: z.infer) => Effect.Effect = Effect.fn( + "SessionPrompt.loop", + )(function* (input: z.infer) { + return yield* state.ensureRunning(input.sessionID, lastAssistant(input.sessionID), runLoop(input.sessionID)) + }) - yield* plugin.trigger( - "command.execute.before", - { command: input.command, sessionID: input.sessionID, arguments: input.arguments }, - { parts }, + const shell: (input: ShellInput) => Effect.Effect = Effect.fn("SessionPrompt.shell")( + function* (input: ShellInput) { + return yield* state.startShell(input.sessionID, lastAssistant(input.sessionID), shellImpl(input)) + }, + ) + + const command = Effect.fn("SessionPrompt.command")(function* (input: CommandInput) { + yield* elog.info("command", { sessionID: input.sessionID, command: input.command, agent: input.agent }) + const cmd = yield* commands.get(input.command) + if (!cmd) { + const available = (yield* commands.list()).map((c) => c.name) + const hint = available.length ? ` Available commands: ${available.join(", ")}` : "" + const error = new NamedError.Unknown({ message: `Command not found: "${input.command}".${hint}` }) + yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() }) + throw error + } + const agentName = cmd.agent ?? input.agent ?? (yield* agents.defaultAgent()) + + const raw = input.arguments.match(argsRegex) ?? [] + const args = raw.map((arg) => arg.replace(quoteTrimRegex, "")) + const templateCommand = yield* Effect.promise(async () => cmd.template) + + const placeholders = templateCommand.match(placeholderRegex) ?? [] + let last = 0 + for (const item of placeholders) { + const value = Number(item.slice(1)) + if (value > last) last = value + } + + const withArgs = templateCommand.replaceAll(placeholderRegex, (_, index) => { + const position = Number(index) + const argIndex = position - 1 + if (argIndex >= args.length) return "" + if (position === last) return args.slice(argIndex).join(" ") + return args[argIndex] + }) + const usesArgumentsPlaceholder = templateCommand.includes("$ARGUMENTS") + let template = withArgs.replaceAll("$ARGUMENTS", input.arguments) + + if (placeholders.length === 0 && !usesArgumentsPlaceholder && input.arguments.trim()) { + template = template + "\n\n" + input.arguments + } + + const shellMatches = ConfigMarkdown.shell(template) + if (shellMatches.length > 0) { + const sh = Shell.preferred() + const results = yield* Effect.promise(() => + Promise.all( + shellMatches.map(async ([, cmd]) => (await Process.text([cmd], { shell: sh, nothrow: true })).text), + ), ) + let index = 0 + template = template.replace(bashRegex, () => results[index++]) + } + template = template.trim() - const result = yield* prompt({ - sessionID: input.sessionID, - messageID: input.messageID, - model: userModel, - agent: userAgent, - parts, - variant: input.variant, - }) - yield* bus.publish(Command.Event.Executed, { - name: input.command, - sessionID: input.sessionID, - arguments: input.arguments, - messageID: result.info.id, - }) - return result + const taskModel = yield* Effect.gen(function* () { + if (cmd.model) return Provider.parseModel(cmd.model) + if (cmd.agent) { + const cmdAgent = yield* agents.get(cmd.agent) + if (cmdAgent?.model) return cmdAgent.model + } + if (input.model) return Provider.parseModel(input.model) + return yield* lastModel(input.sessionID) }) - return Service.of({ - cancel, - prompt, - loop, - shell, - command, - resolvePromptParts, - }) - }), - ) + yield* getModel(taskModel.providerID, taskModel.modelID, input.sessionID) - export const defaultLayer = Layer.suspend(() => - layer.pipe( - Layer.provide(SessionRunState.defaultLayer), - Layer.provide(SessionStatus.defaultLayer), - Layer.provide(SessionCompaction.defaultLayer), - Layer.provide(SessionProcessor.defaultLayer), - Layer.provide(Command.defaultLayer), - Layer.provide(Permission.defaultLayer), - Layer.provide(MCP.defaultLayer), - Layer.provide(LSP.defaultLayer), - Layer.provide(FileTime.defaultLayer), - Layer.provide(ToolRegistry.defaultLayer), - Layer.provide(Truncate.defaultLayer), - Layer.provide(Provider.defaultLayer), - Layer.provide(Instruction.defaultLayer), - Layer.provide(AppFileSystem.defaultLayer), - Layer.provide(Plugin.defaultLayer), - Layer.provide(Session.defaultLayer), - Layer.provide(SessionRevert.defaultLayer), - Layer.provide(SessionSummary.defaultLayer), - Layer.provide( - Layer.mergeAll( - Agent.defaultLayer, - SystemPrompt.defaultLayer, - LLM.defaultLayer, - Bus.layer, - CrossSpawnSpawner.defaultLayer, - ), + const agent = yield* agents.get(agentName) + if (!agent) { + const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) + const hint = available.length ? ` Available agents: ${available.join(", ")}` : "" + const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` }) + yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() }) + throw error + } + + const templateParts = yield* resolvePromptParts(template) + const isSubtask = (agent.mode === "subagent" && cmd.subtask !== false) || cmd.subtask === true + const parts = isSubtask + ? [ + { + type: "subtask" as const, + agent: agent.name, + description: cmd.description ?? "", + command: input.command, + model: { providerID: taskModel.providerID, modelID: taskModel.modelID }, + prompt: templateParts.find((y) => y.type === "text")?.text ?? "", + }, + ] + : [...templateParts, ...(input.parts ?? [])] + + const userAgent = isSubtask ? (input.agent ?? (yield* agents.defaultAgent())) : agentName + const userModel = isSubtask + ? input.model + ? Provider.parseModel(input.model) + : yield* lastModel(input.sessionID) + : taskModel + + yield* plugin.trigger( + "command.execute.before", + { command: input.command, sessionID: input.sessionID, arguments: input.arguments }, + { parts }, + ) + + const result = yield* prompt({ + sessionID: input.sessionID, + messageID: input.messageID, + model: userModel, + agent: userAgent, + parts, + variant: input.variant, + }) + yield* bus.publish(Command.Event.Executed, { + name: input.command, + sessionID: input.sessionID, + arguments: input.arguments, + messageID: result.info.id, + }) + return result + }) + + return Service.of({ + cancel, + prompt, + loop, + shell, + command, + resolvePromptParts, + }) + }), +) + +export const defaultLayer = Layer.suspend(() => + layer.pipe( + Layer.provide(SessionRunState.defaultLayer), + Layer.provide(SessionStatus.defaultLayer), + Layer.provide(SessionCompaction.defaultLayer), + Layer.provide(SessionProcessor.defaultLayer), + Layer.provide(Command.defaultLayer), + Layer.provide(Permission.defaultLayer), + Layer.provide(MCP.defaultLayer), + Layer.provide(LSP.defaultLayer), + Layer.provide(FileTime.defaultLayer), + Layer.provide(ToolRegistry.defaultLayer), + Layer.provide(Truncate.defaultLayer), + Layer.provide(Provider.defaultLayer), + Layer.provide(Instruction.defaultLayer), + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(Plugin.defaultLayer), + Layer.provide(Session.defaultLayer), + Layer.provide(SessionRevert.defaultLayer), + Layer.provide(SessionSummary.defaultLayer), + Layer.provide( + Layer.mergeAll( + Agent.defaultLayer, + SystemPrompt.defaultLayer, + LLM.defaultLayer, + Bus.layer, + CrossSpawnSpawner.defaultLayer, ), ), - ) - export const PromptInput = z.object({ - sessionID: SessionID.zod, - messageID: MessageID.zod.optional(), - model: z - .object({ - providerID: ProviderID.zod, - modelID: ModelID.zod, + ), +) +export const PromptInput = z.object({ + sessionID: SessionID.zod, + messageID: MessageID.zod.optional(), + model: z + .object({ + providerID: ProviderID.zod, + modelID: ModelID.zod, + }) + .optional(), + agent: z.string().optional(), + noReply: z.boolean().optional(), + tools: z + .record(z.string(), z.boolean()) + .optional() + .describe( + "@deprecated tools and permissions have been merged, you can set permissions on the session itself now", + ), + format: MessageV2.Format.optional(), + system: z.string().optional(), + variant: z.string().optional(), + parts: z.array( + z.discriminatedUnion("type", [ + MessageV2.TextPart.omit({ + messageID: true, + sessionID: true, }) - .optional(), - agent: z.string().optional(), - noReply: z.boolean().optional(), - tools: z - .record(z.string(), z.boolean()) - .optional() - .describe( - "@deprecated tools and permissions have been merged, you can set permissions on the session itself now", - ), - format: MessageV2.Format.optional(), - system: z.string().optional(), - variant: z.string().optional(), - parts: z.array( - z.discriminatedUnion("type", [ - MessageV2.TextPart.omit({ - messageID: true, - sessionID: true, + .partial({ + id: true, }) - .partial({ - id: true, - }) - .meta({ - ref: "TextPartInput", - }), + .meta({ + ref: "TextPartInput", + }), + MessageV2.FilePart.omit({ + messageID: true, + sessionID: true, + }) + .partial({ + id: true, + }) + .meta({ + ref: "FilePartInput", + }), + MessageV2.AgentPart.omit({ + messageID: true, + sessionID: true, + }) + .partial({ + id: true, + }) + .meta({ + ref: "AgentPartInput", + }), + MessageV2.SubtaskPart.omit({ + messageID: true, + sessionID: true, + }) + .partial({ + id: true, + }) + .meta({ + ref: "SubtaskPartInput", + }), + ]), + ), +}) +export type PromptInput = z.infer + +export const LoopInput = z.object({ + sessionID: SessionID.zod, +}) + +export const ShellInput = z.object({ + sessionID: SessionID.zod, + messageID: MessageID.zod.optional(), + agent: z.string(), + model: z + .object({ + providerID: ProviderID.zod, + modelID: ModelID.zod, + }) + .optional(), + command: z.string(), +}) +export type ShellInput = z.infer + +export const CommandInput = z.object({ + messageID: MessageID.zod.optional(), + sessionID: SessionID.zod, + agent: z.string().optional(), + model: z.string().optional(), + arguments: z.string(), + command: z.string(), + variant: z.string().optional(), + parts: z + .array( + z.discriminatedUnion("type", [ MessageV2.FilePart.omit({ messageID: true, sessionID: true, - }) - .partial({ - id: true, - }) - .meta({ - ref: "FilePartInput", - }), - MessageV2.AgentPart.omit({ - messageID: true, - sessionID: true, - }) - .partial({ - id: true, - }) - .meta({ - ref: "AgentPartInput", - }), - MessageV2.SubtaskPart.omit({ - messageID: true, - sessionID: true, - }) - .partial({ - id: true, - }) - .meta({ - ref: "SubtaskPartInput", - }), + }).partial({ + id: true, + }), ]), - ), + ) + .optional(), +}) +export type CommandInput = z.infer + +/** @internal Exported for testing */ +export function createStructuredOutputTool(input: { + schema: Record + onSuccess: (output: unknown) => void +}): AITool { + // Remove $schema property if present (not needed for tool input) + const { $schema: _, ...toolSchema } = input.schema + + return tool({ + description: STRUCTURED_OUTPUT_DESCRIPTION, + inputSchema: jsonSchema(toolSchema as JSONSchema7), + async execute(args) { + // AI SDK validates args against inputSchema before calling execute() + input.onSuccess(args) + return { + output: "Structured output captured successfully.", + title: "Structured Output", + metadata: { valid: true }, + } + }, + toModelOutput({ output }) { + return { + type: "text", + value: output.output, + } + }, }) - export type PromptInput = z.infer - - export const LoopInput = z.object({ - sessionID: SessionID.zod, - }) - - export const ShellInput = z.object({ - sessionID: SessionID.zod, - messageID: MessageID.zod.optional(), - agent: z.string(), - model: z - .object({ - providerID: ProviderID.zod, - modelID: ModelID.zod, - }) - .optional(), - command: z.string(), - }) - export type ShellInput = z.infer - - export const CommandInput = z.object({ - messageID: MessageID.zod.optional(), - sessionID: SessionID.zod, - agent: z.string().optional(), - model: z.string().optional(), - arguments: z.string(), - command: z.string(), - variant: z.string().optional(), - parts: z - .array( - z.discriminatedUnion("type", [ - MessageV2.FilePart.omit({ - messageID: true, - sessionID: true, - }).partial({ - id: true, - }), - ]), - ) - .optional(), - }) - export type CommandInput = z.infer - - /** @internal Exported for testing */ - export function createStructuredOutputTool(input: { - schema: Record - onSuccess: (output: unknown) => void - }): AITool { - // Remove $schema property if present (not needed for tool input) - const { $schema: _, ...toolSchema } = input.schema - - return tool({ - description: STRUCTURED_OUTPUT_DESCRIPTION, - inputSchema: jsonSchema(toolSchema as JSONSchema7), - async execute(args) { - // AI SDK validates args against inputSchema before calling execute() - input.onSuccess(args) - return { - output: "Structured output captured successfully.", - title: "Structured Output", - metadata: { valid: true }, - } - }, - toModelOutput({ output }) { - return { - type: "text", - value: output.output, - } - }, - }) - } - const bashRegex = /!`([^`]+)`/g - // Match [Image N] as single token, quoted strings, or non-space sequences - const argsRegex = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi - const placeholderRegex = /\$(\d+)/g - const quoteTrimRegex = /^["']|["']$/g } +const bashRegex = /!`([^`]+)`/g +// Match [Image N] as single token, quoted strings, or non-space sequences +const argsRegex = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi +const placeholderRegex = /\$(\d+)/g +const quoteTrimRegex = /^["']|["']$/g + +export * as SessionPrompt from "./prompt" diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index 6aad55f3f8..12fd4d345d 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -3,123 +3,123 @@ import { Cause, Clock, Duration, Effect, Schedule } from "effect" import { MessageV2 } from "./message-v2" import { iife } from "@/util/iife" -export namespace SessionRetry { - export type Err = ReturnType +export type Err = ReturnType - // This exported message is shared with the TUI upsell detector. Matching on a - // literal error string kind of sucks, but it is the simplest for now. - export const GO_UPSELL_MESSAGE = "Free usage exceeded, subscribe to Go https://opencode.ai/go" +// This exported message is shared with the TUI upsell detector. Matching on a +// literal error string kind of sucks, but it is the simplest for now. +export const GO_UPSELL_MESSAGE = "Free usage exceeded, subscribe to Go https://opencode.ai/go" - export const RETRY_INITIAL_DELAY = 2000 - export const RETRY_BACKOFF_FACTOR = 2 - export const RETRY_MAX_DELAY_NO_HEADERS = 30_000 // 30 seconds - export const RETRY_MAX_DELAY = 2_147_483_647 // max 32-bit signed integer for setTimeout +export const RETRY_INITIAL_DELAY = 2000 +export const RETRY_BACKOFF_FACTOR = 2 +export const RETRY_MAX_DELAY_NO_HEADERS = 30_000 // 30 seconds +export const RETRY_MAX_DELAY = 2_147_483_647 // max 32-bit signed integer for setTimeout - function cap(ms: number) { - return Math.min(ms, RETRY_MAX_DELAY) - } - - export function delay(attempt: number, error?: MessageV2.APIError) { - if (error) { - const headers = error.data.responseHeaders - if (headers) { - const retryAfterMs = headers["retry-after-ms"] - if (retryAfterMs) { - const parsedMs = Number.parseFloat(retryAfterMs) - if (!Number.isNaN(parsedMs)) { - return cap(parsedMs) - } - } - - const retryAfter = headers["retry-after"] - if (retryAfter) { - const parsedSeconds = Number.parseFloat(retryAfter) - if (!Number.isNaN(parsedSeconds)) { - // convert seconds to milliseconds - return cap(Math.ceil(parsedSeconds * 1000)) - } - // Try parsing as HTTP date format - const parsed = Date.parse(retryAfter) - Date.now() - if (!Number.isNaN(parsed) && parsed > 0) { - return cap(Math.ceil(parsed)) - } - } - - return cap(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1)) - } - } - - return cap(Math.min(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1), RETRY_MAX_DELAY_NO_HEADERS)) - } - - export function retryable(error: Err) { - // context overflow errors should not be retried - if (MessageV2.ContextOverflowError.isInstance(error)) return undefined - if (MessageV2.APIError.isInstance(error)) { - const status = error.data.statusCode - // 5xx errors are transient server failures and should always be retried, - // even when the provider SDK doesn't explicitly mark them as retryable. - if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined - if (error.data.responseBody?.includes("FreeUsageLimitError")) return GO_UPSELL_MESSAGE - return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message - } - - // Check for rate limit patterns in plain text error messages - const msg = error.data?.message - if (typeof msg === "string") { - const lower = msg.toLowerCase() - if ( - lower.includes("rate increased too quickly") || - lower.includes("rate limit") || - lower.includes("too many requests") - ) { - return msg - } - } - - const json = iife(() => { - try { - if (typeof error.data?.message === "string") { - const parsed = JSON.parse(error.data.message) - return parsed - } - - return JSON.parse(error.data.message) - } catch { - return undefined - } - }) - if (!json || typeof json !== "object") return undefined - const code = typeof json.code === "string" ? json.code : "" - - if (json.type === "error" && json.error?.type === "too_many_requests") { - return "Too Many Requests" - } - if (code.includes("exhausted") || code.includes("unavailable")) { - return "Provider is overloaded" - } - if (json.type === "error" && typeof json.error?.code === "string" && json.error.code.includes("rate_limit")) { - return "Rate Limited" - } - return undefined - } - - export function policy(opts: { - parse: (error: unknown) => Err - set: (input: { attempt: number; message: string; next: number }) => Effect.Effect - }) { - return Schedule.fromStepWithMetadata( - Effect.succeed((meta: Schedule.InputMetadata) => { - const error = opts.parse(meta.input) - const message = retryable(error) - if (!message) return Cause.done(meta.attempt) - return Effect.gen(function* () { - const wait = delay(meta.attempt, MessageV2.APIError.isInstance(error) ? error : undefined) - const now = yield* Clock.currentTimeMillis - yield* opts.set({ attempt: meta.attempt, message, next: now + wait }) - return [meta.attempt, Duration.millis(wait)] as [number, Duration.Duration] - }) - }), - ) - } +function cap(ms: number) { + return Math.min(ms, RETRY_MAX_DELAY) } + +export function delay(attempt: number, error?: MessageV2.APIError) { + if (error) { + const headers = error.data.responseHeaders + if (headers) { + const retryAfterMs = headers["retry-after-ms"] + if (retryAfterMs) { + const parsedMs = Number.parseFloat(retryAfterMs) + if (!Number.isNaN(parsedMs)) { + return cap(parsedMs) + } + } + + const retryAfter = headers["retry-after"] + if (retryAfter) { + const parsedSeconds = Number.parseFloat(retryAfter) + if (!Number.isNaN(parsedSeconds)) { + // convert seconds to milliseconds + return cap(Math.ceil(parsedSeconds * 1000)) + } + // Try parsing as HTTP date format + const parsed = Date.parse(retryAfter) - Date.now() + if (!Number.isNaN(parsed) && parsed > 0) { + return cap(Math.ceil(parsed)) + } + } + + return cap(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1)) + } + } + + return cap(Math.min(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1), RETRY_MAX_DELAY_NO_HEADERS)) +} + +export function retryable(error: Err) { + // context overflow errors should not be retried + if (MessageV2.ContextOverflowError.isInstance(error)) return undefined + if (MessageV2.APIError.isInstance(error)) { + const status = error.data.statusCode + // 5xx errors are transient server failures and should always be retried, + // even when the provider SDK doesn't explicitly mark them as retryable. + if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined + if (error.data.responseBody?.includes("FreeUsageLimitError")) return GO_UPSELL_MESSAGE + return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message + } + + // Check for rate limit patterns in plain text error messages + const msg = error.data?.message + if (typeof msg === "string") { + const lower = msg.toLowerCase() + if ( + lower.includes("rate increased too quickly") || + lower.includes("rate limit") || + lower.includes("too many requests") + ) { + return msg + } + } + + const json = iife(() => { + try { + if (typeof error.data?.message === "string") { + const parsed = JSON.parse(error.data.message) + return parsed + } + + return JSON.parse(error.data.message) + } catch { + return undefined + } + }) + if (!json || typeof json !== "object") return undefined + const code = typeof json.code === "string" ? json.code : "" + + if (json.type === "error" && json.error?.type === "too_many_requests") { + return "Too Many Requests" + } + if (code.includes("exhausted") || code.includes("unavailable")) { + return "Provider is overloaded" + } + if (json.type === "error" && typeof json.error?.code === "string" && json.error.code.includes("rate_limit")) { + return "Rate Limited" + } + return undefined +} + +export function policy(opts: { + parse: (error: unknown) => Err + set: (input: { attempt: number; message: string; next: number }) => Effect.Effect +}) { + return Schedule.fromStepWithMetadata( + Effect.succeed((meta: Schedule.InputMetadata) => { + const error = opts.parse(meta.input) + const message = retryable(error) + if (!message) return Cause.done(meta.attempt) + return Effect.gen(function* () { + const wait = delay(meta.attempt, MessageV2.APIError.isInstance(error) ? error : undefined) + const now = yield* Clock.currentTimeMillis + yield* opts.set({ attempt: meta.attempt, message, next: now + wait }) + return [meta.attempt, Duration.millis(wait)] as [number, Duration.Duration] + }) + }), + ) +} + +export * as SessionRetry from "./retry" diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index f09ccf24ad..c7e5220f12 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -11,151 +11,151 @@ import { SessionID, MessageID, PartID } from "./schema" import { SessionRunState } from "./run-state" import { SessionSummary } from "./summary" -export namespace SessionRevert { - const log = Log.create({ service: "session.revert" }) +const log = Log.create({ service: "session.revert" }) - export const RevertInput = z.object({ - sessionID: SessionID.zod, - messageID: MessageID.zod, - partID: PartID.zod.optional(), - }) - export type RevertInput = z.infer +export const RevertInput = z.object({ + sessionID: SessionID.zod, + messageID: MessageID.zod, + partID: PartID.zod.optional(), +}) +export type RevertInput = z.infer - export interface Interface { - readonly revert: (input: RevertInput) => Effect.Effect - readonly unrevert: (input: { sessionID: SessionID }) => Effect.Effect - readonly cleanup: (session: Session.Info) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/SessionRevert") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const sessions = yield* Session.Service - const snap = yield* Snapshot.Service - const storage = yield* Storage.Service - const bus = yield* Bus.Service - const summary = yield* SessionSummary.Service - const state = yield* SessionRunState.Service - - const revert = Effect.fn("SessionRevert.revert")(function* (input: RevertInput) { - yield* state.assertNotBusy(input.sessionID) - const all = yield* sessions.messages({ sessionID: input.sessionID }) - let lastUser: MessageV2.User | undefined - const session = yield* sessions.get(input.sessionID) - - let rev: Session.Info["revert"] - const patches: Snapshot.Patch[] = [] - for (const msg of all) { - if (msg.info.role === "user") lastUser = msg.info - const remaining = [] - for (const part of msg.parts) { - if (rev) { - if (part.type === "patch") patches.push(part) - continue - } - - if (!rev) { - if ((msg.info.id === input.messageID && !input.partID) || part.id === input.partID) { - const partID = remaining.some((item) => ["text", "tool"].includes(item.type)) ? input.partID : undefined - rev = { - messageID: !partID && lastUser ? lastUser.id : msg.info.id, - partID, - } - } - remaining.push(part) - } - } - } - - if (!rev) return session - - rev.snapshot = session.revert?.snapshot ?? (yield* snap.track()) - if (session.revert?.snapshot) yield* snap.restore(session.revert.snapshot) - yield* snap.revert(patches) - if (rev.snapshot) rev.diff = yield* snap.diff(rev.snapshot as string) - const range = all.filter((msg) => msg.info.id >= rev!.messageID) - const diffs = yield* summary.computeDiff({ messages: range }) - yield* storage.write(["session_diff", input.sessionID], diffs).pipe(Effect.ignore) - yield* bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs }) - yield* sessions.setRevert({ - sessionID: input.sessionID, - revert: rev, - summary: { - additions: diffs.reduce((sum, x) => sum + x.additions, 0), - deletions: diffs.reduce((sum, x) => sum + x.deletions, 0), - files: diffs.length, - }, - }) - return yield* sessions.get(input.sessionID) - }) - - const unrevert = Effect.fn("SessionRevert.unrevert")(function* (input: { sessionID: SessionID }) { - log.info("unreverting", input) - yield* state.assertNotBusy(input.sessionID) - const session = yield* sessions.get(input.sessionID) - if (!session.revert) return session - if (session.revert.snapshot) yield* snap.restore(session.revert!.snapshot!) - yield* sessions.clearRevert(input.sessionID) - return yield* sessions.get(input.sessionID) - }) - - const cleanup = Effect.fn("SessionRevert.cleanup")(function* (session: Session.Info) { - if (!session.revert) return - const sessionID = session.id - const msgs = yield* sessions.messages({ sessionID }) - const messageID = session.revert.messageID - const remove = [] as MessageV2.WithParts[] - let target: MessageV2.WithParts | undefined - for (const msg of msgs) { - if (msg.info.id < messageID) continue - if (msg.info.id > messageID) { - remove.push(msg) - continue - } - if (session.revert.partID) { - target = msg - continue - } - remove.push(msg) - } - for (const msg of remove) { - SyncEvent.run(MessageV2.Event.Removed, { - sessionID, - messageID: msg.info.id, - }) - } - if (session.revert.partID && target) { - const partID = session.revert.partID - const idx = target.parts.findIndex((part) => part.id === partID) - if (idx >= 0) { - const removeParts = target.parts.slice(idx) - target.parts = target.parts.slice(0, idx) - for (const part of removeParts) { - SyncEvent.run(MessageV2.Event.PartRemoved, { - sessionID, - messageID: target.info.id, - partID: part.id, - }) - } - } - } - yield* sessions.clearRevert(sessionID) - }) - - return Service.of({ revert, unrevert, cleanup }) - }), - ) - - export const defaultLayer = Layer.suspend(() => - layer.pipe( - Layer.provide(SessionRunState.defaultLayer), - Layer.provide(Session.defaultLayer), - Layer.provide(Snapshot.defaultLayer), - Layer.provide(Storage.defaultLayer), - Layer.provide(Bus.layer), - Layer.provide(SessionSummary.defaultLayer), - ), - ) +export interface Interface { + readonly revert: (input: RevertInput) => Effect.Effect + readonly unrevert: (input: { sessionID: SessionID }) => Effect.Effect + readonly cleanup: (session: Session.Info) => Effect.Effect } + +export class Service extends Context.Service()("@opencode/SessionRevert") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const sessions = yield* Session.Service + const snap = yield* Snapshot.Service + const storage = yield* Storage.Service + const bus = yield* Bus.Service + const summary = yield* SessionSummary.Service + const state = yield* SessionRunState.Service + + const revert = Effect.fn("SessionRevert.revert")(function* (input: RevertInput) { + yield* state.assertNotBusy(input.sessionID) + const all = yield* sessions.messages({ sessionID: input.sessionID }) + let lastUser: MessageV2.User | undefined + const session = yield* sessions.get(input.sessionID) + + let rev: Session.Info["revert"] + const patches: Snapshot.Patch[] = [] + for (const msg of all) { + if (msg.info.role === "user") lastUser = msg.info + const remaining = [] + for (const part of msg.parts) { + if (rev) { + if (part.type === "patch") patches.push(part) + continue + } + + if (!rev) { + if ((msg.info.id === input.messageID && !input.partID) || part.id === input.partID) { + const partID = remaining.some((item) => ["text", "tool"].includes(item.type)) ? input.partID : undefined + rev = { + messageID: !partID && lastUser ? lastUser.id : msg.info.id, + partID, + } + } + remaining.push(part) + } + } + } + + if (!rev) return session + + rev.snapshot = session.revert?.snapshot ?? (yield* snap.track()) + if (session.revert?.snapshot) yield* snap.restore(session.revert.snapshot) + yield* snap.revert(patches) + if (rev.snapshot) rev.diff = yield* snap.diff(rev.snapshot as string) + const range = all.filter((msg) => msg.info.id >= rev!.messageID) + const diffs = yield* summary.computeDiff({ messages: range }) + yield* storage.write(["session_diff", input.sessionID], diffs).pipe(Effect.ignore) + yield* bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs }) + yield* sessions.setRevert({ + sessionID: input.sessionID, + revert: rev, + summary: { + additions: diffs.reduce((sum, x) => sum + x.additions, 0), + deletions: diffs.reduce((sum, x) => sum + x.deletions, 0), + files: diffs.length, + }, + }) + return yield* sessions.get(input.sessionID) + }) + + const unrevert = Effect.fn("SessionRevert.unrevert")(function* (input: { sessionID: SessionID }) { + log.info("unreverting", input) + yield* state.assertNotBusy(input.sessionID) + const session = yield* sessions.get(input.sessionID) + if (!session.revert) return session + if (session.revert.snapshot) yield* snap.restore(session.revert!.snapshot!) + yield* sessions.clearRevert(input.sessionID) + return yield* sessions.get(input.sessionID) + }) + + const cleanup = Effect.fn("SessionRevert.cleanup")(function* (session: Session.Info) { + if (!session.revert) return + const sessionID = session.id + const msgs = yield* sessions.messages({ sessionID }) + const messageID = session.revert.messageID + const remove = [] as MessageV2.WithParts[] + let target: MessageV2.WithParts | undefined + for (const msg of msgs) { + if (msg.info.id < messageID) continue + if (msg.info.id > messageID) { + remove.push(msg) + continue + } + if (session.revert.partID) { + target = msg + continue + } + remove.push(msg) + } + for (const msg of remove) { + SyncEvent.run(MessageV2.Event.Removed, { + sessionID, + messageID: msg.info.id, + }) + } + if (session.revert.partID && target) { + const partID = session.revert.partID + const idx = target.parts.findIndex((part) => part.id === partID) + if (idx >= 0) { + const removeParts = target.parts.slice(idx) + target.parts = target.parts.slice(0, idx) + for (const part of removeParts) { + SyncEvent.run(MessageV2.Event.PartRemoved, { + sessionID, + messageID: target.info.id, + partID: part.id, + }) + } + } + } + yield* sessions.clearRevert(sessionID) + }) + + return Service.of({ revert, unrevert, cleanup }) + }), +) + +export const defaultLayer = Layer.suspend(() => + layer.pipe( + Layer.provide(SessionRunState.defaultLayer), + Layer.provide(Session.defaultLayer), + Layer.provide(Snapshot.defaultLayer), + Layer.provide(Storage.defaultLayer), + Layer.provide(Bus.layer), + Layer.provide(SessionSummary.defaultLayer), + ), +) + +export * as SessionRevert from "./revert" diff --git a/packages/opencode/src/session/run-state.ts b/packages/opencode/src/session/run-state.ts index a18e0b5732..7a106f8a4c 100644 --- a/packages/opencode/src/session/run-state.ts +++ b/packages/opencode/src/session/run-state.ts @@ -6,103 +6,103 @@ import { MessageV2 } from "./message-v2" import { SessionID } from "./schema" import { SessionStatus } from "./status" -export namespace SessionRunState { - export interface Interface { - readonly assertNotBusy: (sessionID: SessionID) => Effect.Effect - readonly cancel: (sessionID: SessionID) => Effect.Effect - readonly ensureRunning: ( - sessionID: SessionID, - onInterrupt: Effect.Effect, - work: Effect.Effect, - ) => Effect.Effect - readonly startShell: ( - sessionID: SessionID, - onInterrupt: Effect.Effect, - work: Effect.Effect, - ) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/SessionRunState") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const status = yield* SessionStatus.Service - - const state = yield* InstanceState.make( - Effect.fn("SessionRunState.state")(function* () { - const scope = yield* Scope.Scope - const runners = new Map>() - yield* Effect.addFinalizer( - Effect.fnUntraced(function* () { - yield* Effect.forEach(runners.values(), (runner) => runner.cancel, { - concurrency: "unbounded", - discard: true, - }) - runners.clear() - }), - ) - return { runners, scope } - }), - ) - - const runner = Effect.fn("SessionRunState.runner")(function* ( - sessionID: SessionID, - onInterrupt: Effect.Effect, - ) { - const data = yield* InstanceState.get(state) - const existing = data.runners.get(sessionID) - if (existing) return existing - const next = Runner.make(data.scope, { - onIdle: Effect.gen(function* () { - data.runners.delete(sessionID) - yield* status.set(sessionID, { type: "idle" }) - }), - onBusy: status.set(sessionID, { type: "busy" }), - onInterrupt, - busy: () => { - throw new Session.BusyError(sessionID) - }, - }) - data.runners.set(sessionID, next) - return next - }) - - const assertNotBusy = Effect.fn("SessionRunState.assertNotBusy")(function* (sessionID: SessionID) { - const data = yield* InstanceState.get(state) - const existing = data.runners.get(sessionID) - if (existing?.busy) throw new Session.BusyError(sessionID) - }) - - const cancel = Effect.fn("SessionRunState.cancel")(function* (sessionID: SessionID) { - const data = yield* InstanceState.get(state) - const existing = data.runners.get(sessionID) - if (!existing || !existing.busy) { - yield* status.set(sessionID, { type: "idle" }) - return - } - yield* existing.cancel - }) - - const ensureRunning = Effect.fn("SessionRunState.ensureRunning")(function* ( - sessionID: SessionID, - onInterrupt: Effect.Effect, - work: Effect.Effect, - ) { - return yield* (yield* runner(sessionID, onInterrupt)).ensureRunning(work) - }) - - const startShell = Effect.fn("SessionRunState.startShell")(function* ( - sessionID: SessionID, - onInterrupt: Effect.Effect, - work: Effect.Effect, - ) { - return yield* (yield* runner(sessionID, onInterrupt)).startShell(work) - }) - - return Service.of({ assertNotBusy, cancel, ensureRunning, startShell }) - }), - ) - - export const defaultLayer = layer.pipe(Layer.provide(SessionStatus.defaultLayer)) +export interface Interface { + readonly assertNotBusy: (sessionID: SessionID) => Effect.Effect + readonly cancel: (sessionID: SessionID) => Effect.Effect + readonly ensureRunning: ( + sessionID: SessionID, + onInterrupt: Effect.Effect, + work: Effect.Effect, + ) => Effect.Effect + readonly startShell: ( + sessionID: SessionID, + onInterrupt: Effect.Effect, + work: Effect.Effect, + ) => Effect.Effect } + +export class Service extends Context.Service()("@opencode/SessionRunState") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const status = yield* SessionStatus.Service + + const state = yield* InstanceState.make( + Effect.fn("SessionRunState.state")(function* () { + const scope = yield* Scope.Scope + const runners = new Map>() + yield* Effect.addFinalizer( + Effect.fnUntraced(function* () { + yield* Effect.forEach(runners.values(), (runner) => runner.cancel, { + concurrency: "unbounded", + discard: true, + }) + runners.clear() + }), + ) + return { runners, scope } + }), + ) + + const runner = Effect.fn("SessionRunState.runner")(function* ( + sessionID: SessionID, + onInterrupt: Effect.Effect, + ) { + const data = yield* InstanceState.get(state) + const existing = data.runners.get(sessionID) + if (existing) return existing + const next = Runner.make(data.scope, { + onIdle: Effect.gen(function* () { + data.runners.delete(sessionID) + yield* status.set(sessionID, { type: "idle" }) + }), + onBusy: status.set(sessionID, { type: "busy" }), + onInterrupt, + busy: () => { + throw new Session.BusyError(sessionID) + }, + }) + data.runners.set(sessionID, next) + return next + }) + + const assertNotBusy = Effect.fn("SessionRunState.assertNotBusy")(function* (sessionID: SessionID) { + const data = yield* InstanceState.get(state) + const existing = data.runners.get(sessionID) + if (existing?.busy) throw new Session.BusyError(sessionID) + }) + + const cancel = Effect.fn("SessionRunState.cancel")(function* (sessionID: SessionID) { + const data = yield* InstanceState.get(state) + const existing = data.runners.get(sessionID) + if (!existing || !existing.busy) { + yield* status.set(sessionID, { type: "idle" }) + return + } + yield* existing.cancel + }) + + const ensureRunning = Effect.fn("SessionRunState.ensureRunning")(function* ( + sessionID: SessionID, + onInterrupt: Effect.Effect, + work: Effect.Effect, + ) { + return yield* (yield* runner(sessionID, onInterrupt)).ensureRunning(work) + }) + + const startShell = Effect.fn("SessionRunState.startShell")(function* ( + sessionID: SessionID, + onInterrupt: Effect.Effect, + work: Effect.Effect, + ) { + return yield* (yield* runner(sessionID, onInterrupt)).startShell(work) + }) + + return Service.of({ assertNotBusy, cancel, ensureRunning, startShell }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(SessionStatus.defaultLayer)) + +export * as SessionRunState from "./run-state" diff --git a/packages/opencode/src/session/status.ts b/packages/opencode/src/session/status.ts index f0d4e6cf79..7f46c70a8a 100644 --- a/packages/opencode/src/session/status.ts +++ b/packages/opencode/src/session/status.ts @@ -5,84 +5,84 @@ import { SessionID } from "./schema" import { Effect, Layer, Context } from "effect" import z from "zod" -export namespace SessionStatus { - export const Info = z - .union([ - z.object({ - type: z.literal("idle"), - }), - z.object({ - type: z.literal("retry"), - attempt: z.number(), - message: z.string(), - next: z.number(), - }), - z.object({ - type: z.literal("busy"), - }), - ]) - .meta({ - ref: "SessionStatus", - }) - export type Info = z.infer - - export const Event = { - Status: BusEvent.define( - "session.status", - z.object({ - sessionID: SessionID.zod, - status: Info, - }), - ), - // deprecated - Idle: BusEvent.define( - "session.idle", - z.object({ - sessionID: SessionID.zod, - }), - ), - } - - export interface Interface { - readonly get: (sessionID: SessionID) => Effect.Effect - readonly list: () => Effect.Effect> - readonly set: (sessionID: SessionID, status: Info) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/SessionStatus") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const bus = yield* Bus.Service - - const state = yield* InstanceState.make( - Effect.fn("SessionStatus.state")(() => Effect.succeed(new Map())), - ) - - const get = Effect.fn("SessionStatus.get")(function* (sessionID: SessionID) { - const data = yield* InstanceState.get(state) - return data.get(sessionID) ?? { type: "idle" as const } - }) - - const list = Effect.fn("SessionStatus.list")(function* () { - return new Map(yield* InstanceState.get(state)) - }) - - const set = Effect.fn("SessionStatus.set")(function* (sessionID: SessionID, status: Info) { - const data = yield* InstanceState.get(state) - yield* bus.publish(Event.Status, { sessionID, status }) - if (status.type === "idle") { - yield* bus.publish(Event.Idle, { sessionID }) - data.delete(sessionID) - return - } - data.set(sessionID, status) - }) - - return Service.of({ get, list, set }) +export const Info = z + .union([ + z.object({ + type: z.literal("idle"), }), - ) + z.object({ + type: z.literal("retry"), + attempt: z.number(), + message: z.string(), + next: z.number(), + }), + z.object({ + type: z.literal("busy"), + }), + ]) + .meta({ + ref: "SessionStatus", + }) +export type Info = z.infer - export const defaultLayer = layer.pipe(Layer.provide(Bus.layer)) +export const Event = { + Status: BusEvent.define( + "session.status", + z.object({ + sessionID: SessionID.zod, + status: Info, + }), + ), + // deprecated + Idle: BusEvent.define( + "session.idle", + z.object({ + sessionID: SessionID.zod, + }), + ), } + +export interface Interface { + readonly get: (sessionID: SessionID) => Effect.Effect + readonly list: () => Effect.Effect> + readonly set: (sessionID: SessionID, status: Info) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/SessionStatus") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const bus = yield* Bus.Service + + const state = yield* InstanceState.make( + Effect.fn("SessionStatus.state")(() => Effect.succeed(new Map())), + ) + + const get = Effect.fn("SessionStatus.get")(function* (sessionID: SessionID) { + const data = yield* InstanceState.get(state) + return data.get(sessionID) ?? { type: "idle" as const } + }) + + const list = Effect.fn("SessionStatus.list")(function* () { + return new Map(yield* InstanceState.get(state)) + }) + + const set = Effect.fn("SessionStatus.set")(function* (sessionID: SessionID, status: Info) { + const data = yield* InstanceState.get(state) + yield* bus.publish(Event.Status, { sessionID, status }) + if (status.type === "idle") { + yield* bus.publish(Event.Idle, { sessionID }) + data.delete(sessionID) + return + } + data.set(sessionID, status) + }) + + return Service.of({ get, list, set }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(Bus.layer)) + +export * as SessionStatus from "./status" diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index 9f8e70f162..2be08f3f43 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -7,159 +7,159 @@ import * as Session from "./session" import { MessageV2 } from "./message-v2" import { SessionID, MessageID } from "./schema" -export namespace SessionSummary { - function unquoteGitPath(input: string) { - if (!input.startsWith('"')) return input - if (!input.endsWith('"')) return input - const body = input.slice(1, -1) - const bytes: number[] = [] +function unquoteGitPath(input: string) { + if (!input.startsWith('"')) return input + if (!input.endsWith('"')) return input + const body = input.slice(1, -1) + const bytes: number[] = [] - for (let i = 0; i < body.length; i++) { - const char = body[i]! - if (char !== "\\") { - bytes.push(char.charCodeAt(0)) - continue - } - - const next = body[i + 1] - if (!next) { - bytes.push("\\".charCodeAt(0)) - continue - } - - if (next >= "0" && next <= "7") { - const chunk = body.slice(i + 1, i + 4) - const match = chunk.match(/^[0-7]{1,3}/) - if (!match) { - bytes.push(next.charCodeAt(0)) - i++ - continue - } - bytes.push(parseInt(match[0], 8)) - i += match[0].length - continue - } - - const escaped = - next === "n" - ? "\n" - : next === "r" - ? "\r" - : next === "t" - ? "\t" - : next === "b" - ? "\b" - : next === "f" - ? "\f" - : next === "v" - ? "\v" - : next === "\\" || next === '"' - ? next - : undefined - - bytes.push((escaped ?? next).charCodeAt(0)) - i++ + for (let i = 0; i < body.length; i++) { + const char = body[i]! + if (char !== "\\") { + bytes.push(char.charCodeAt(0)) + continue } - return Buffer.from(bytes).toString() + const next = body[i + 1] + if (!next) { + bytes.push("\\".charCodeAt(0)) + continue + } + + if (next >= "0" && next <= "7") { + const chunk = body.slice(i + 1, i + 4) + const match = chunk.match(/^[0-7]{1,3}/) + if (!match) { + bytes.push(next.charCodeAt(0)) + i++ + continue + } + bytes.push(parseInt(match[0], 8)) + i += match[0].length + continue + } + + const escaped = + next === "n" + ? "\n" + : next === "r" + ? "\r" + : next === "t" + ? "\t" + : next === "b" + ? "\b" + : next === "f" + ? "\f" + : next === "v" + ? "\v" + : next === "\\" || next === '"' + ? next + : undefined + + bytes.push((escaped ?? next).charCodeAt(0)) + i++ } - export interface Interface { - readonly summarize: (input: { sessionID: SessionID; messageID: MessageID }) => Effect.Effect - readonly diff: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect - readonly computeDiff: (input: { messages: MessageV2.WithParts[] }) => Effect.Effect - } + return Buffer.from(bytes).toString() +} - export class Service extends Context.Service()("@opencode/SessionSummary") {} +export interface Interface { + readonly summarize: (input: { sessionID: SessionID; messageID: MessageID }) => Effect.Effect + readonly diff: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect + readonly computeDiff: (input: { messages: MessageV2.WithParts[] }) => Effect.Effect +} - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const sessions = yield* Session.Service - const snapshot = yield* Snapshot.Service - const storage = yield* Storage.Service - const bus = yield* Bus.Service +export class Service extends Context.Service()("@opencode/SessionSummary") {} - const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: { - messages: MessageV2.WithParts[] - }) { - let from: string | undefined - let to: string | undefined - for (const item of input.messages) { - if (!from) { - for (const part of item.parts) { - if (part.type === "step-start" && part.snapshot) { - from = part.snapshot - break - } +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const sessions = yield* Session.Service + const snapshot = yield* Snapshot.Service + const storage = yield* Storage.Service + const bus = yield* Bus.Service + + const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: { + messages: MessageV2.WithParts[] + }) { + let from: string | undefined + let to: string | undefined + for (const item of input.messages) { + if (!from) { + for (const part of item.parts) { + if (part.type === "step-start" && part.snapshot) { + from = part.snapshot + break } } - for (const part of item.parts) { - if (part.type === "step-finish" && part.snapshot) to = part.snapshot - } } - if (from && to) return yield* snapshot.diffFull(from, to) - return [] + for (const part of item.parts) { + if (part.type === "step-finish" && part.snapshot) to = part.snapshot + } + } + if (from && to) return yield* snapshot.diffFull(from, to) + return [] + }) + + const summarize = Effect.fn("SessionSummary.summarize")(function* (input: { + sessionID: SessionID + messageID: MessageID + }) { + const all = yield* sessions.messages({ sessionID: input.sessionID }) + if (!all.length) return + + const diffs = yield* computeDiff({ messages: all }) + yield* sessions.setSummary({ + sessionID: input.sessionID, + summary: { + additions: diffs.reduce((sum, x) => sum + x.additions, 0), + deletions: diffs.reduce((sum, x) => sum + x.deletions, 0), + files: diffs.length, + }, }) + yield* storage.write(["session_diff", input.sessionID], diffs).pipe(Effect.ignore) + yield* bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs }) - const summarize = Effect.fn("SessionSummary.summarize")(function* (input: { - sessionID: SessionID - messageID: MessageID - }) { - const all = yield* sessions.messages({ sessionID: input.sessionID }) - if (!all.length) return + const messages = all.filter( + (m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID), + ) + const target = messages.find((m) => m.info.id === input.messageID) + if (!target || target.info.role !== "user") return + const msgDiffs = yield* computeDiff({ messages }) + target.info.summary = { ...target.info.summary, diffs: msgDiffs } + yield* sessions.updateMessage(target.info) + }) - const diffs = yield* computeDiff({ messages: all }) - yield* sessions.setSummary({ - sessionID: input.sessionID, - summary: { - additions: diffs.reduce((sum, x) => sum + x.additions, 0), - deletions: diffs.reduce((sum, x) => sum + x.deletions, 0), - files: diffs.length, - }, - }) - yield* storage.write(["session_diff", input.sessionID], diffs).pipe(Effect.ignore) - yield* bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs }) - - const messages = all.filter( - (m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID), - ) - const target = messages.find((m) => m.info.id === input.messageID) - if (!target || target.info.role !== "user") return - const msgDiffs = yield* computeDiff({ messages }) - target.info.summary = { ...target.info.summary, diffs: msgDiffs } - yield* sessions.updateMessage(target.info) + const diff = Effect.fn("SessionSummary.diff")(function* (input: { sessionID: SessionID; messageID?: MessageID }) { + const diffs = yield* storage + .read(["session_diff", input.sessionID]) + .pipe(Effect.catch(() => Effect.succeed([] as Snapshot.FileDiff[]))) + const next = diffs.map((item) => { + const file = unquoteGitPath(item.file) + if (file === item.file) return item + return { ...item, file } }) + const changed = next.some((item, i) => item.file !== diffs[i]?.file) + if (changed) yield* storage.write(["session_diff", input.sessionID], next).pipe(Effect.ignore) + return next + }) - const diff = Effect.fn("SessionSummary.diff")(function* (input: { sessionID: SessionID; messageID?: MessageID }) { - const diffs = yield* storage - .read(["session_diff", input.sessionID]) - .pipe(Effect.catch(() => Effect.succeed([] as Snapshot.FileDiff[]))) - const next = diffs.map((item) => { - const file = unquoteGitPath(item.file) - if (file === item.file) return item - return { ...item, file } - }) - const changed = next.some((item, i) => item.file !== diffs[i]?.file) - if (changed) yield* storage.write(["session_diff", input.sessionID], next).pipe(Effect.ignore) - return next - }) + return Service.of({ summarize, diff, computeDiff }) + }), +) - return Service.of({ summarize, diff, computeDiff }) - }), - ) +export const defaultLayer = Layer.suspend(() => + layer.pipe( + Layer.provide(Session.defaultLayer), + Layer.provide(Snapshot.defaultLayer), + Layer.provide(Storage.defaultLayer), + Layer.provide(Bus.layer), + ), +) - export const defaultLayer = Layer.suspend(() => - layer.pipe( - Layer.provide(Session.defaultLayer), - Layer.provide(Snapshot.defaultLayer), - Layer.provide(Storage.defaultLayer), - Layer.provide(Bus.layer), - ), - ) +export const DiffInput = z.object({ + sessionID: SessionID.zod, + messageID: MessageID.zod.optional(), +}) - export const DiffInput = z.object({ - sessionID: SessionID.zod, - messageID: MessageID.zod.optional(), - }) -} +export * as SessionSummary from "./summary" diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 952ff5b04b..ec60f6eef7 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -16,69 +16,69 @@ import type { Agent } from "@/agent/agent" import { Permission } from "@/permission" import { Skill } from "@/skill" -export namespace SystemPrompt { - export function provider(model: Provider.Model) { - if (model.api.id.includes("gpt-4") || model.api.id.includes("o1") || model.api.id.includes("o3")) - return [PROMPT_BEAST] - if (model.api.id.includes("gpt")) { - if (model.api.id.includes("codex")) { - return [PROMPT_CODEX] - } - return [PROMPT_GPT] +export function provider(model: Provider.Model) { + if (model.api.id.includes("gpt-4") || model.api.id.includes("o1") || model.api.id.includes("o3")) + return [PROMPT_BEAST] + if (model.api.id.includes("gpt")) { + if (model.api.id.includes("codex")) { + return [PROMPT_CODEX] } - if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI] - if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC] - if (model.api.id.toLowerCase().includes("trinity")) return [PROMPT_TRINITY] - if (model.api.id.toLowerCase().includes("kimi")) return [PROMPT_KIMI] - return [PROMPT_DEFAULT] + return [PROMPT_GPT] } - - export interface Interface { - readonly environment: (model: Provider.Model) => string[] - readonly skills: (agent: Agent.Info) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/SystemPrompt") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const skill = yield* Skill.Service - - return Service.of({ - environment(model) { - const project = Instance.project - return [ - [ - `You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`, - `Here is some useful information about the environment you are running in:`, - ``, - ` Working directory: ${Instance.directory}`, - ` Workspace root folder: ${Instance.worktree}`, - ` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`, - ` Platform: ${process.platform}`, - ` Today's date: ${new Date().toDateString()}`, - ``, - ].join("\n"), - ] - }, - - skills: Effect.fn("SystemPrompt.skills")(function* (agent: Agent.Info) { - if (Permission.disabled(["skill"], agent.permission).has("skill")) return - - const list = yield* skill.available(agent) - - return [ - "Skills provide specialized instructions and workflows for specific tasks.", - "Use the skill tool to load a skill when a task matches its description.", - // the agents seem to ingest the information about skills a bit better if we present a more verbose - // version of them here and a less verbose version in tool description, rather than vice versa. - Skill.fmt(list, { verbose: true }), - ].join("\n") - }), - }) - }), - ) - - export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer)) + if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI] + if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC] + if (model.api.id.toLowerCase().includes("trinity")) return [PROMPT_TRINITY] + if (model.api.id.toLowerCase().includes("kimi")) return [PROMPT_KIMI] + return [PROMPT_DEFAULT] } + +export interface Interface { + readonly environment: (model: Provider.Model) => string[] + readonly skills: (agent: Agent.Info) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/SystemPrompt") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const skill = yield* Skill.Service + + return Service.of({ + environment(model) { + const project = Instance.project + return [ + [ + `You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`, + `Here is some useful information about the environment you are running in:`, + ``, + ` Working directory: ${Instance.directory}`, + ` Workspace root folder: ${Instance.worktree}`, + ` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`, + ` Platform: ${process.platform}`, + ` Today's date: ${new Date().toDateString()}`, + ``, + ].join("\n"), + ] + }, + + skills: Effect.fn("SystemPrompt.skills")(function* (agent: Agent.Info) { + if (Permission.disabled(["skill"], agent.permission).has("skill")) return + + const list = yield* skill.available(agent) + + return [ + "Skills provide specialized instructions and workflows for specific tasks.", + "Use the skill tool to load a skill when a task matches its description.", + // the agents seem to ingest the information about skills a bit better if we present a more verbose + // version of them here and a less verbose version in tool description, rather than vice versa. + Skill.fmt(list, { verbose: true }), + ].join("\n") + }), + }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer)) + +export * as SystemPrompt from "./system" diff --git a/packages/opencode/src/session/todo.ts b/packages/opencode/src/session/todo.ts index eec2bb3a30..5523fdc86a 100644 --- a/packages/opencode/src/session/todo.ts +++ b/packages/opencode/src/session/todo.ts @@ -6,80 +6,80 @@ import z from "zod" import { Database, eq, asc } from "../storage" import { TodoTable } from "./session.sql" -export namespace Todo { - export const Info = z - .object({ - content: z.string().describe("Brief description of the task"), - status: z.string().describe("Current status of the task: pending, in_progress, completed, cancelled"), - priority: z.string().describe("Priority level of the task: high, medium, low"), - }) - .meta({ ref: "Todo" }) - export type Info = z.infer +export const Info = z + .object({ + content: z.string().describe("Brief description of the task"), + status: z.string().describe("Current status of the task: pending, in_progress, completed, cancelled"), + priority: z.string().describe("Priority level of the task: high, medium, low"), + }) + .meta({ ref: "Todo" }) +export type Info = z.infer - export const Event = { - Updated: BusEvent.define( - "todo.updated", - z.object({ - sessionID: SessionID.zod, - todos: z.array(Info), - }), - ), - } - - export interface Interface { - readonly update: (input: { sessionID: SessionID; todos: Info[] }) => Effect.Effect - readonly get: (sessionID: SessionID) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/SessionTodo") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const bus = yield* Bus.Service - - const update = Effect.fn("Todo.update")(function* (input: { sessionID: SessionID; todos: Info[] }) { - yield* Effect.sync(() => - Database.transaction((db) => { - db.delete(TodoTable).where(eq(TodoTable.session_id, input.sessionID)).run() - if (input.todos.length === 0) return - db.insert(TodoTable) - .values( - input.todos.map((todo, position) => ({ - session_id: input.sessionID, - content: todo.content, - status: todo.status, - priority: todo.priority, - position, - })), - ) - .run() - }), - ) - yield* bus.publish(Event.Updated, input) - }) - - const get = Effect.fn("Todo.get")(function* (sessionID: SessionID) { - const rows = yield* Effect.sync(() => - Database.use((db) => - db - .select() - .from(TodoTable) - .where(eq(TodoTable.session_id, sessionID)) - .orderBy(asc(TodoTable.position)) - .all(), - ), - ) - return rows.map((row) => ({ - content: row.content, - status: row.status, - priority: row.priority, - })) - }) - - return Service.of({ update, get }) +export const Event = { + Updated: BusEvent.define( + "todo.updated", + z.object({ + sessionID: SessionID.zod, + todos: z.array(Info), }), - ) - - export const defaultLayer = layer.pipe(Layer.provide(Bus.layer)) + ), } + +export interface Interface { + readonly update: (input: { sessionID: SessionID; todos: Info[] }) => Effect.Effect + readonly get: (sessionID: SessionID) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/SessionTodo") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const bus = yield* Bus.Service + + const update = Effect.fn("Todo.update")(function* (input: { sessionID: SessionID; todos: Info[] }) { + yield* Effect.sync(() => + Database.transaction((db) => { + db.delete(TodoTable).where(eq(TodoTable.session_id, input.sessionID)).run() + if (input.todos.length === 0) return + db.insert(TodoTable) + .values( + input.todos.map((todo, position) => ({ + session_id: input.sessionID, + content: todo.content, + status: todo.status, + priority: todo.priority, + position, + })), + ) + .run() + }), + ) + yield* bus.publish(Event.Updated, input) + }) + + const get = Effect.fn("Todo.get")(function* (sessionID: SessionID) { + const rows = yield* Effect.sync(() => + Database.use((db) => + db + .select() + .from(TodoTable) + .where(eq(TodoTable.session_id, sessionID)) + .orderBy(asc(TodoTable.position)) + .all(), + ), + ) + return rows.map((row) => ({ + content: row.content, + status: row.status, + priority: row.priority, + })) + }) + + return Service.of({ update, get }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(Bus.layer)) + +export * as Todo from "./todo" From 266fb9342238a62cf9d66437c81c91ee40e8632c Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 00:50:44 +0000 Subject: [PATCH 035/335] chore: generate --- packages/opencode/src/session/message-v2.ts | 11 ++--------- packages/opencode/src/session/prompt.ts | 12 +++--------- packages/opencode/src/session/summary.ts | 4 +--- packages/opencode/src/session/todo.ts | 7 +------ 4 files changed, 7 insertions(+), 27 deletions(-) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 5e7e008401..46686947e1 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -544,10 +544,7 @@ const part = (row: typeof PartTable.$inferSelect) => }) as Part const older = (row: Cursor) => - or( - lt(MessageTable.time_created, row.time), - and(eq(MessageTable.time_created, row.time), lt(MessageTable.id, row.id)), - ) + or(lt(MessageTable.time_created, row.time), and(eq(MessageTable.time_created, row.time), lt(MessageTable.id, row.id))) function hydrate(rows: (typeof MessageTable.$inferSelect)[]) { const ids = rows.map((row) => row.id) @@ -930,11 +927,7 @@ export function filterCompacted(msgs: Iterable) { const completed = new Set() for (const msg of msgs) { result.push(msg) - if ( - msg.info.role === "user" && - completed.has(msg.info.id) && - msg.parts.some((part) => part.type === "compaction") - ) + if (msg.info.role === "user" && completed.has(msg.info.id) && msg.parts.some((part) => part.type === "compaction")) break if (msg.info.role === "assistant" && msg.info.summary && msg.info.finish && !msg.info.error) completed.add(msg.info.parentID) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 14fdf30780..9faa618788 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1073,9 +1073,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the let start = parseInt(range.start) let end = range.end ? parseInt(range.end) : undefined if (start === end) { - const symbols = yield* lsp - .documentSymbol(filePathURI) - .pipe(Effect.catch(() => Effect.succeed([]))) + const symbols = yield* lsp.documentSymbol(filePathURI).pipe(Effect.catch(() => Effect.succeed([]))) for (const symbol of symbols) { let r: LSP.Range | undefined if ("range" in symbol) r = symbol.range @@ -1453,9 +1451,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the } if (step === 1) - yield* summary - .summarize({ sessionID, messageID: lastUser.id }) - .pipe(Effect.ignore, Effect.forkIn(scope)) + yield* summary.summarize({ sessionID, messageID: lastUser.id }).pipe(Effect.ignore, Effect.forkIn(scope)) if (step > 1 && lastFinished) { for (const m of msgs) { @@ -1723,9 +1719,7 @@ export const PromptInput = z.object({ tools: z .record(z.string(), z.boolean()) .optional() - .describe( - "@deprecated tools and permissions have been merged, you can set permissions on the session itself now", - ), + .describe("@deprecated tools and permissions have been merged, you can set permissions on the session itself now"), format: MessageV2.Format.optional(), system: z.string().optional(), variant: z.string().optional(), diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index 2be08f3f43..70b3102f6e 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -79,9 +79,7 @@ export const layer = Layer.effect( const storage = yield* Storage.Service const bus = yield* Bus.Service - const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: { - messages: MessageV2.WithParts[] - }) { + const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: { messages: MessageV2.WithParts[] }) { let from: string | undefined let to: string | undefined for (const item of input.messages) { diff --git a/packages/opencode/src/session/todo.ts b/packages/opencode/src/session/todo.ts index 5523fdc86a..4840f86a3d 100644 --- a/packages/opencode/src/session/todo.ts +++ b/packages/opencode/src/session/todo.ts @@ -61,12 +61,7 @@ export const layer = Layer.effect( const get = Effect.fn("Todo.get")(function* (sessionID: SessionID) { const rows = yield* Effect.sync(() => Database.use((db) => - db - .select() - .from(TodoTable) - .where(eq(TodoTable.session_id, sessionID)) - .orderBy(asc(TodoTable.position)) - .all(), + db.select().from(TodoTable).where(eq(TodoTable.session_id, sessionID)).orderBy(asc(TodoTable.position)).all(), ), ) return rows.map((row) => ({ From d2cb1613ace83609db6a60f3065914e322f93d02 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 20:59:42 -0400 Subject: [PATCH 036/335] refactor: unwrap SessionEntry namespace + self-reexport (#22977) --- packages/opencode/src/v2/session-entry.ts | 618 +++++++++++----------- 1 file changed, 309 insertions(+), 309 deletions(-) diff --git a/packages/opencode/src/v2/session-entry.ts b/packages/opencode/src/v2/session-entry.ts index 490f184227..140fa47d23 100644 --- a/packages/opencode/src/v2/session-entry.ts +++ b/packages/opencode/src/v2/session-entry.ts @@ -2,317 +2,317 @@ import { Schema } from "effect" import { SessionEvent } from "./session-event" import { produce } from "immer" -export namespace SessionEntry { - export const ID = SessionEvent.ID - export type ID = Schema.Schema.Type +export const ID = SessionEvent.ID +export type ID = Schema.Schema.Type - const Base = { - id: SessionEvent.ID, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - time: Schema.Struct({ - created: Schema.DateTimeUtc, - }), - } +const Base = { + id: SessionEvent.ID, + metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + time: Schema.Struct({ + created: Schema.DateTimeUtc, + }), +} - export class User extends Schema.Class("Session.Entry.User")({ - ...Base, - text: SessionEvent.Prompt.fields.text, - files: SessionEvent.Prompt.fields.files, - agents: SessionEvent.Prompt.fields.agents, - type: Schema.Literal("user"), - time: Schema.Struct({ - created: Schema.DateTimeUtc, - }), - }) { - static fromEvent(event: SessionEvent.Prompt) { - return new User({ - id: event.id, - type: "user", - metadata: event.metadata, - text: event.text, - files: event.files, - agents: event.agents, - time: { created: event.timestamp }, - }) - } - } - - export class Synthetic extends Schema.Class("Session.Entry.Synthetic")({ - ...SessionEvent.Synthetic.fields, - ...Base, - type: Schema.Literal("synthetic"), - }) {} - - export class ToolStatePending extends Schema.Class("Session.Entry.ToolState.Pending")({ - status: Schema.Literal("pending"), - input: Schema.String, - }) {} - - export class ToolStateRunning extends Schema.Class("Session.Entry.ToolState.Running")({ - status: Schema.Literal("running"), - input: Schema.Record(Schema.String, Schema.Unknown), - title: Schema.String.pipe(Schema.optional), - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - }) {} - - export class ToolStateCompleted extends Schema.Class("Session.Entry.ToolState.Completed")({ - status: Schema.Literal("completed"), - input: Schema.Record(Schema.String, Schema.Unknown), - output: Schema.String, - title: Schema.String, - metadata: Schema.Record(Schema.String, Schema.Unknown), - attachments: SessionEvent.FileAttachment.pipe(Schema.Array, Schema.optional), - }) {} - - export class ToolStateError extends Schema.Class("Session.Entry.ToolState.Error")({ - status: Schema.Literal("error"), - input: Schema.Record(Schema.String, Schema.Unknown), - error: Schema.String, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - }) {} - - export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]) - export type ToolState = Schema.Schema.Type - - export class AssistantTool extends Schema.Class("Session.Entry.Assistant.Tool")({ - type: Schema.Literal("tool"), - callID: Schema.String, - name: Schema.String, - state: ToolState, - time: Schema.Struct({ - created: Schema.DateTimeUtc, - ran: Schema.DateTimeUtc.pipe(Schema.optional), - completed: Schema.DateTimeUtc.pipe(Schema.optional), - pruned: Schema.DateTimeUtc.pipe(Schema.optional), - }), - }) {} - - export class AssistantText extends Schema.Class("Session.Entry.Assistant.Text")({ - type: Schema.Literal("text"), - text: Schema.String, - }) {} - - export class AssistantReasoning extends Schema.Class("Session.Entry.Assistant.Reasoning")({ - type: Schema.Literal("reasoning"), - text: Schema.String, - }) {} - - export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]) - export type AssistantContent = Schema.Schema.Type - - export class Assistant extends Schema.Class("Session.Entry.Assistant")({ - ...Base, - type: Schema.Literal("assistant"), - content: AssistantContent.pipe(Schema.Array), - cost: Schema.Number.pipe(Schema.optional), - tokens: Schema.Struct({ - input: Schema.Number, - output: Schema.Number, - reasoning: Schema.Number, - cache: Schema.Struct({ - read: Schema.Number, - write: Schema.Number, - }), - }).pipe(Schema.optional), - error: Schema.String.pipe(Schema.optional), - time: Schema.Struct({ - created: Schema.DateTimeUtc, - completed: Schema.DateTimeUtc.pipe(Schema.optional), - }), - }) {} - - export class Compaction extends Schema.Class("Session.Entry.Compaction")({ - ...SessionEvent.Compacted.fields, - type: Schema.Literal("compaction"), - ...Base, - }) {} - - export const Entry = Schema.Union([User, Synthetic, Assistant, Compaction]) - - export type Entry = Schema.Schema.Type - - export type Type = Entry["type"] - - export type History = { - entries: Entry[] - pending: Entry[] - } - - export function step(old: History, event: SessionEvent.Event): History { - return produce(old, (draft) => { - const lastAssistant = draft.entries.findLast((x) => x.type === "assistant") - const pendingAssistant = lastAssistant && !lastAssistant.time.completed ? lastAssistant : undefined - - switch (event.type) { - case "prompt": { - if (pendingAssistant) { - // @ts-expect-error - draft.pending.push(User.fromEvent(event)) - break - } - // @ts-expect-error - draft.entries.push(User.fromEvent(event)) - break - } - case "step.started": { - if (pendingAssistant) pendingAssistant.time.completed = event.timestamp - draft.entries.push({ - id: event.id, - type: "assistant", - time: { - created: event.timestamp, - }, - content: [], - }) - break - } - case "text.started": { - if (!pendingAssistant) break - pendingAssistant.content.push({ - type: "text", - text: "", - }) - break - } - case "text.delta": { - if (!pendingAssistant) break - const match = pendingAssistant.content.findLast((x) => x.type === "text") - if (match) match.text += event.delta - break - } - case "text.ended": { - break - } - case "tool.input.started": { - if (!pendingAssistant) break - pendingAssistant.content.push({ - type: "tool", - callID: event.callID, - name: event.name, - time: { - created: event.timestamp, - }, - state: { - status: "pending", - input: "", - }, - }) - break - } - case "tool.input.delta": { - if (!pendingAssistant) break - const match = pendingAssistant.content.findLast((x) => x.type === "tool") - // oxlint-disable-next-line no-base-to-string -- event.delta is a Schema.String (runtime string) - if (match) match.state.input += event.delta - break - } - case "tool.input.ended": { - break - } - case "tool.called": { - if (!pendingAssistant) break - const match = pendingAssistant.content.findLast((x) => x.type === "tool") - if (match) { - match.time.ran = event.timestamp - match.state = { - status: "running", - input: event.input, - } - } - break - } - case "tool.success": { - if (!pendingAssistant) break - const match = pendingAssistant.content.findLast((x) => x.type === "tool") - if (match && match.state.status === "running") { - match.state = { - status: "completed", - input: match.state.input, - output: event.output ?? "", - title: event.title, - metadata: event.metadata ?? {}, - // @ts-expect-error - attachments: event.attachments ?? [], - } - } - break - } - case "tool.error": { - if (!pendingAssistant) break - const match = pendingAssistant.content.findLast((x) => x.type === "tool") - if (match && match.state.status === "running") { - match.state = { - status: "error", - error: event.error, - input: match.state.input, - metadata: event.metadata ?? {}, - } - } - break - } - case "reasoning.started": { - if (!pendingAssistant) break - pendingAssistant.content.push({ - type: "reasoning", - text: "", - }) - break - } - case "reasoning.delta": { - if (!pendingAssistant) break - const match = pendingAssistant.content.findLast((x) => x.type === "reasoning") - if (match) match.text += event.delta - break - } - case "reasoning.ended": { - if (!pendingAssistant) break - const match = pendingAssistant.content.findLast((x) => x.type === "reasoning") - if (match) match.text = event.text - break - } - case "step.ended": { - if (!pendingAssistant) break - pendingAssistant.time.completed = event.timestamp - pendingAssistant.cost = event.cost - pendingAssistant.tokens = event.tokens - break - } - } +export class User extends Schema.Class("Session.Entry.User")({ + ...Base, + text: SessionEvent.Prompt.fields.text, + files: SessionEvent.Prompt.fields.files, + agents: SessionEvent.Prompt.fields.agents, + type: Schema.Literal("user"), + time: Schema.Struct({ + created: Schema.DateTimeUtc, + }), +}) { + static fromEvent(event: SessionEvent.Prompt) { + return new User({ + id: event.id, + type: "user", + metadata: event.metadata, + text: event.text, + files: event.files, + agents: event.agents, + time: { created: event.timestamp }, }) } - - /* - export interface Interface { - readonly decode: (row: typeof SessionEntryTable.$inferSelect) => Entry - readonly fromSession: (sessionID: SessionID) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/SessionEntry") {} - - export const layer: Layer.Layer = Layer.effect( - Service, - Effect.gen(function* () { - const decodeEntry = Schema.decodeUnknownSync(Entry) - - const decode: (typeof Service.Service)["decode"] = (row) => decodeEntry({ ...row, id: row.id, type: row.type }) - - const fromSession = Effect.fn("SessionEntry.fromSession")(function* (sessionID: SessionID) { - return Database.use((db) => - db - .select() - .from(SessionEntryTable) - .where(eq(SessionEntryTable.session_id, sessionID)) - .orderBy(SessionEntryTable.id) - .all() - .map((row) => decode(row)), - ) - }) - - return Service.of({ - decode, - fromSession, - }) - }), - ) - */ } + +export class Synthetic extends Schema.Class("Session.Entry.Synthetic")({ + ...SessionEvent.Synthetic.fields, + ...Base, + type: Schema.Literal("synthetic"), +}) {} + +export class ToolStatePending extends Schema.Class("Session.Entry.ToolState.Pending")({ + status: Schema.Literal("pending"), + input: Schema.String, +}) {} + +export class ToolStateRunning extends Schema.Class("Session.Entry.ToolState.Running")({ + status: Schema.Literal("running"), + input: Schema.Record(Schema.String, Schema.Unknown), + title: Schema.String.pipe(Schema.optional), + metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), +}) {} + +export class ToolStateCompleted extends Schema.Class("Session.Entry.ToolState.Completed")({ + status: Schema.Literal("completed"), + input: Schema.Record(Schema.String, Schema.Unknown), + output: Schema.String, + title: Schema.String, + metadata: Schema.Record(Schema.String, Schema.Unknown), + attachments: SessionEvent.FileAttachment.pipe(Schema.Array, Schema.optional), +}) {} + +export class ToolStateError extends Schema.Class("Session.Entry.ToolState.Error")({ + status: Schema.Literal("error"), + input: Schema.Record(Schema.String, Schema.Unknown), + error: Schema.String, + metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), +}) {} + +export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]) +export type ToolState = Schema.Schema.Type + +export class AssistantTool extends Schema.Class("Session.Entry.Assistant.Tool")({ + type: Schema.Literal("tool"), + callID: Schema.String, + name: Schema.String, + state: ToolState, + time: Schema.Struct({ + created: Schema.DateTimeUtc, + ran: Schema.DateTimeUtc.pipe(Schema.optional), + completed: Schema.DateTimeUtc.pipe(Schema.optional), + pruned: Schema.DateTimeUtc.pipe(Schema.optional), + }), +}) {} + +export class AssistantText extends Schema.Class("Session.Entry.Assistant.Text")({ + type: Schema.Literal("text"), + text: Schema.String, +}) {} + +export class AssistantReasoning extends Schema.Class("Session.Entry.Assistant.Reasoning")({ + type: Schema.Literal("reasoning"), + text: Schema.String, +}) {} + +export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]) +export type AssistantContent = Schema.Schema.Type + +export class Assistant extends Schema.Class("Session.Entry.Assistant")({ + ...Base, + type: Schema.Literal("assistant"), + content: AssistantContent.pipe(Schema.Array), + cost: Schema.Number.pipe(Schema.optional), + tokens: Schema.Struct({ + input: Schema.Number, + output: Schema.Number, + reasoning: Schema.Number, + cache: Schema.Struct({ + read: Schema.Number, + write: Schema.Number, + }), + }).pipe(Schema.optional), + error: Schema.String.pipe(Schema.optional), + time: Schema.Struct({ + created: Schema.DateTimeUtc, + completed: Schema.DateTimeUtc.pipe(Schema.optional), + }), +}) {} + +export class Compaction extends Schema.Class("Session.Entry.Compaction")({ + ...SessionEvent.Compacted.fields, + type: Schema.Literal("compaction"), + ...Base, +}) {} + +export const Entry = Schema.Union([User, Synthetic, Assistant, Compaction]) + +export type Entry = Schema.Schema.Type + +export type Type = Entry["type"] + +export type History = { + entries: Entry[] + pending: Entry[] +} + +export function step(old: History, event: SessionEvent.Event): History { + return produce(old, (draft) => { + const lastAssistant = draft.entries.findLast((x) => x.type === "assistant") + const pendingAssistant = lastAssistant && !lastAssistant.time.completed ? lastAssistant : undefined + + switch (event.type) { + case "prompt": { + if (pendingAssistant) { + // @ts-expect-error + draft.pending.push(User.fromEvent(event)) + break + } + // @ts-expect-error + draft.entries.push(User.fromEvent(event)) + break + } + case "step.started": { + if (pendingAssistant) pendingAssistant.time.completed = event.timestamp + draft.entries.push({ + id: event.id, + type: "assistant", + time: { + created: event.timestamp, + }, + content: [], + }) + break + } + case "text.started": { + if (!pendingAssistant) break + pendingAssistant.content.push({ + type: "text", + text: "", + }) + break + } + case "text.delta": { + if (!pendingAssistant) break + const match = pendingAssistant.content.findLast((x) => x.type === "text") + if (match) match.text += event.delta + break + } + case "text.ended": { + break + } + case "tool.input.started": { + if (!pendingAssistant) break + pendingAssistant.content.push({ + type: "tool", + callID: event.callID, + name: event.name, + time: { + created: event.timestamp, + }, + state: { + status: "pending", + input: "", + }, + }) + break + } + case "tool.input.delta": { + if (!pendingAssistant) break + const match = pendingAssistant.content.findLast((x) => x.type === "tool") + // oxlint-disable-next-line no-base-to-string -- event.delta is a Schema.String (runtime string) + if (match) match.state.input += event.delta + break + } + case "tool.input.ended": { + break + } + case "tool.called": { + if (!pendingAssistant) break + const match = pendingAssistant.content.findLast((x) => x.type === "tool") + if (match) { + match.time.ran = event.timestamp + match.state = { + status: "running", + input: event.input, + } + } + break + } + case "tool.success": { + if (!pendingAssistant) break + const match = pendingAssistant.content.findLast((x) => x.type === "tool") + if (match && match.state.status === "running") { + match.state = { + status: "completed", + input: match.state.input, + output: event.output ?? "", + title: event.title, + metadata: event.metadata ?? {}, + // @ts-expect-error + attachments: event.attachments ?? [], + } + } + break + } + case "tool.error": { + if (!pendingAssistant) break + const match = pendingAssistant.content.findLast((x) => x.type === "tool") + if (match && match.state.status === "running") { + match.state = { + status: "error", + error: event.error, + input: match.state.input, + metadata: event.metadata ?? {}, + } + } + break + } + case "reasoning.started": { + if (!pendingAssistant) break + pendingAssistant.content.push({ + type: "reasoning", + text: "", + }) + break + } + case "reasoning.delta": { + if (!pendingAssistant) break + const match = pendingAssistant.content.findLast((x) => x.type === "reasoning") + if (match) match.text += event.delta + break + } + case "reasoning.ended": { + if (!pendingAssistant) break + const match = pendingAssistant.content.findLast((x) => x.type === "reasoning") + if (match) match.text = event.text + break + } + case "step.ended": { + if (!pendingAssistant) break + pendingAssistant.time.completed = event.timestamp + pendingAssistant.cost = event.cost + pendingAssistant.tokens = event.tokens + break + } + } + }) +} + +/* +export interface Interface { + readonly decode: (row: typeof SessionEntryTable.$inferSelect) => Entry + readonly fromSession: (sessionID: SessionID) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/SessionEntry") {} + +export const layer: Layer.Layer = Layer.effect( + Service, + Effect.gen(function* () { + const decodeEntry = Schema.decodeUnknownSync(Entry) + + const decode: (typeof Service.Service)["decode"] = (row) => decodeEntry({ ...row, id: row.id, type: row.type }) + + const fromSession = Effect.fn("SessionEntry.fromSession")(function* (sessionID: SessionID) { + return Database.use((db) => + db + .select() + .from(SessionEntryTable) + .where(eq(SessionEntryTable.session_id, sessionID)) + .orderBy(SessionEntryTable.id) + .all() + .map((row) => decode(row)), + ) + }) + + return Service.of({ + decode, + fromSession, + }) + }), +) +*/ + +export * as SessionEntry from "./session-entry" From 54046e0b985d8ffd5e343cadcc479570b96f8a5b Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 21:00:30 -0400 Subject: [PATCH 037/335] refactor: unwrap SessionV2 namespace + self-reexport (#22978) --- packages/opencode/src/v2/session.ts | 114 ++++++++++++++-------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/packages/opencode/src/v2/session.ts b/packages/opencode/src/v2/session.ts index ce1b39031f..79a6916120 100644 --- a/packages/opencode/src/v2/session.ts +++ b/packages/opencode/src/v2/session.ts @@ -4,66 +4,66 @@ import { Struct } from "effect" import { Session } from "@/session" import { SessionID } from "@/session/schema" -export namespace SessionV2 { - export const ID = SessionID +export const ID = SessionID - export type ID = Schema.Schema.Type +export type ID = Schema.Schema.Type - export class PromptInput extends Schema.Class("Session.PromptInput")({ - ...Struct.omit(SessionEntry.User.fields, ["time", "type"]), - id: Schema.optionalKey(SessionEntry.ID), - sessionID: SessionV2.ID, - }) {} +export class PromptInput extends Schema.Class("Session.PromptInput")({ + ...Struct.omit(SessionEntry.User.fields, ["time", "type"]), + id: Schema.optionalKey(SessionEntry.ID), + sessionID: ID, +}) {} - export class CreateInput extends Schema.Class("Session.CreateInput")({ - id: Schema.optionalKey(SessionV2.ID), - }) {} +export class CreateInput extends Schema.Class("Session.CreateInput")({ + id: Schema.optionalKey(ID), +}) {} - export class Info extends Schema.Class("Session.Info")({ - id: SessionV2.ID, - model: Schema.Struct({ - id: Schema.String, - providerID: Schema.String, - modelID: Schema.String, - }).pipe(Schema.optional), - }) {} +export class Info extends Schema.Class("Session.Info")({ + id: ID, + model: Schema.Struct({ + id: Schema.String, + providerID: Schema.String, + modelID: Schema.String, + }).pipe(Schema.optional), +}) {} - export interface Interface { - fromID: (id: SessionV2.ID) => Effect.Effect - create: (input: CreateInput) => Effect.Effect - prompt: (input: PromptInput) => Effect.Effect - } - - export class Service extends Context.Service()("Session.Service") {} - - export const layer = Layer.effect(Service)( - Effect.gen(function* () { - const session = yield* Session.Service - - const create: Interface["create"] = Effect.fn("Session.create")(function* (_input) { - throw new Error("Not implemented") - }) - - const prompt: Interface["prompt"] = Effect.fn("Session.prompt")(function* (_input) { - throw new Error("Not implemented") - }) - - const fromID: Interface["fromID"] = Effect.fn("Session.fromID")(function* (id) { - const match = yield* session.get(id) - return fromV1(match) - }) - - return Service.of({ - create, - prompt, - fromID, - }) - }), - ) - - function fromV1(input: Session.Info): Info { - return new Info({ - id: SessionV2.ID.make(input.id), - }) - } +export interface Interface { + fromID: (id: ID) => Effect.Effect + create: (input: CreateInput) => Effect.Effect + prompt: (input: PromptInput) => Effect.Effect } + +export class Service extends Context.Service()("Session.Service") {} + +export const layer = Layer.effect(Service)( + Effect.gen(function* () { + const session = yield* Session.Service + + const create: Interface["create"] = Effect.fn("Session.create")(function* (_input) { + throw new Error("Not implemented") + }) + + const prompt: Interface["prompt"] = Effect.fn("Session.prompt")(function* (_input) { + throw new Error("Not implemented") + }) + + const fromID: Interface["fromID"] = Effect.fn("Session.fromID")(function* (id) { + const match = yield* session.get(id) + return fromV1(match) + }) + + return Service.of({ + create, + prompt, + fromID, + }) + }), +) + +function fromV1(input: Session.Info): Info { + return new Info({ + id: ID.make(input.id), + }) +} + +export * as SessionV2 from "./session" From 5022895e2b9b556275c5cd419cb32452329ada08 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 21:01:24 -0400 Subject: [PATCH 038/335] refactor: unwrap ExperimentalHttpApiServer namespace + self-reexport (#22979) --- .../src/server/instance/httpapi/server.ts | 202 +++++++++--------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/packages/opencode/src/server/instance/httpapi/server.ts b/packages/opencode/src/server/instance/httpapi/server.ts index 299a177f50..362d0970b9 100644 --- a/packages/opencode/src/server/instance/httpapi/server.ts +++ b/packages/opencode/src/server/instance/httpapi/server.ts @@ -25,106 +25,106 @@ const Headers = Schema.Struct({ "x-opencode-directory": Schema.optional(Schema.String), }) -export namespace ExperimentalHttpApiServer { - function decode(input: string) { - try { - return decodeURIComponent(input) - } catch { - return input - } +function decode(input: string) { + try { + return decodeURIComponent(input) + } catch { + return input } - - class Unauthorized extends Schema.TaggedErrorClass()( - "Unauthorized", - { message: Schema.String }, - { httpApiStatus: 401 }, - ) {} - - class Authorization extends HttpApiMiddleware.Service()("@opencode/ExperimentalHttpApiAuthorization", { - error: Unauthorized, - security: { - basic: HttpApiSecurity.basic, - }, - }) {} - - const normalize = HttpRouter.middleware()( - Effect.gen(function* () { - return (effect) => - Effect.gen(function* () { - const query = yield* HttpServerRequest.schemaSearchParams(Query) - if (!query.auth_token) return yield* effect - const req = yield* HttpServerRequest.HttpServerRequest - const next = req.modify({ - headers: { - ...req.headers, - authorization: `Basic ${query.auth_token}`, - }, - }) - return yield* effect.pipe(Effect.provideService(HttpServerRequest.HttpServerRequest, next)) - }) - }), - ).layer - - const auth = Layer.succeed( - Authorization, - Authorization.of({ - basic: (effect, { credential }) => - Effect.gen(function* () { - if (!Flag.OPENCODE_SERVER_PASSWORD) return yield* effect - - const user = Flag.OPENCODE_SERVER_USERNAME ?? "opencode" - if (credential.username !== user) { - return yield* new Unauthorized({ message: "Unauthorized" }) - } - if (Redacted.value(credential.password) !== Flag.OPENCODE_SERVER_PASSWORD) { - return yield* new Unauthorized({ message: "Unauthorized" }) - } - return yield* effect - }), - }), - ) - - const instance = HttpRouter.middleware()( - Effect.gen(function* () { - return (effect) => - Effect.gen(function* () { - const query = yield* HttpServerRequest.schemaSearchParams(Query) - const headers = yield* HttpServerRequest.schemaHeaders(Headers) - const raw = query.directory || headers["x-opencode-directory"] || process.cwd() - const workspace = query.workspace || undefined - const ctx = yield* Effect.promise(() => - Instance.provide({ - directory: Filesystem.resolve(decode(raw)), - init: () => AppRuntime.runPromise(InstanceBootstrap), - fn: () => Instance.current, - }), - ) - - const next = workspace ? effect.pipe(Effect.provideService(WorkspaceRef, workspace)) : effect - return yield* next.pipe(Effect.provideService(InstanceRef, ctx)) - }) - }), - ).layer - - const QuestionSecured = QuestionApi.middleware(Authorization) - const PermissionSecured = PermissionApi.middleware(Authorization) - const ProviderSecured = ProviderApi.middleware(Authorization) - - export const routes = Layer.mergeAll( - HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)), - HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)), - HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)), - ).pipe( - Layer.provide(auth), - Layer.provide(normalize), - Layer.provide(instance), - Layer.provide(HttpServer.layerServices), - Layer.provideMerge(Observability.layer), - ) - - export const webHandler = lazy(() => - HttpRouter.toWebHandler(routes, { - memoMap, - }), - ) } + +class Unauthorized extends Schema.TaggedErrorClass()( + "Unauthorized", + { message: Schema.String }, + { httpApiStatus: 401 }, +) {} + +class Authorization extends HttpApiMiddleware.Service()("@opencode/ExperimentalHttpApiAuthorization", { + error: Unauthorized, + security: { + basic: HttpApiSecurity.basic, + }, +}) {} + +const normalize = HttpRouter.middleware()( + Effect.gen(function* () { + return (effect) => + Effect.gen(function* () { + const query = yield* HttpServerRequest.schemaSearchParams(Query) + if (!query.auth_token) return yield* effect + const req = yield* HttpServerRequest.HttpServerRequest + const next = req.modify({ + headers: { + ...req.headers, + authorization: `Basic ${query.auth_token}`, + }, + }) + return yield* effect.pipe(Effect.provideService(HttpServerRequest.HttpServerRequest, next)) + }) + }), +).layer + +const auth = Layer.succeed( + Authorization, + Authorization.of({ + basic: (effect, { credential }) => + Effect.gen(function* () { + if (!Flag.OPENCODE_SERVER_PASSWORD) return yield* effect + + const user = Flag.OPENCODE_SERVER_USERNAME ?? "opencode" + if (credential.username !== user) { + return yield* new Unauthorized({ message: "Unauthorized" }) + } + if (Redacted.value(credential.password) !== Flag.OPENCODE_SERVER_PASSWORD) { + return yield* new Unauthorized({ message: "Unauthorized" }) + } + return yield* effect + }), + }), +) + +const instance = HttpRouter.middleware()( + Effect.gen(function* () { + return (effect) => + Effect.gen(function* () { + const query = yield* HttpServerRequest.schemaSearchParams(Query) + const headers = yield* HttpServerRequest.schemaHeaders(Headers) + const raw = query.directory || headers["x-opencode-directory"] || process.cwd() + const workspace = query.workspace || undefined + const ctx = yield* Effect.promise(() => + Instance.provide({ + directory: Filesystem.resolve(decode(raw)), + init: () => AppRuntime.runPromise(InstanceBootstrap), + fn: () => Instance.current, + }), + ) + + const next = workspace ? effect.pipe(Effect.provideService(WorkspaceRef, workspace)) : effect + return yield* next.pipe(Effect.provideService(InstanceRef, ctx)) + }) + }), +).layer + +const QuestionSecured = QuestionApi.middleware(Authorization) +const PermissionSecured = PermissionApi.middleware(Authorization) +const ProviderSecured = ProviderApi.middleware(Authorization) + +export const routes = Layer.mergeAll( + HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)), + HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)), + HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)), +).pipe( + Layer.provide(auth), + Layer.provide(normalize), + Layer.provide(instance), + Layer.provide(HttpServer.layerServices), + Layer.provideMerge(Observability.layer), +) + +export const webHandler = lazy(() => + HttpRouter.toWebHandler(routes, { + memoMap, + }), +) + +export * as ExperimentalHttpApiServer from "./server" From 94878d76f8a32c36909647a0d9e1f6e383f60908 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 21:02:07 -0400 Subject: [PATCH 039/335] refactor: unwrap TuiPluginRuntime namespace + self-reexport (#22980) --- .../src/cli/cmd/tui/plugin/runtime.ts | 194 +++++++++--------- 1 file changed, 97 insertions(+), 97 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts index e1b2eca1dd..e4a0e59eb1 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts @@ -918,113 +918,113 @@ async function installPluginBySpec( } } -export namespace TuiPluginRuntime { - let dir = "" - let loaded: Promise | undefined - let runtime: RuntimeState | undefined - export const Slot = View +let dir = "" +let loaded: Promise | undefined +let runtime: RuntimeState | undefined +export const Slot = View - export async function init(input: { api: HostPluginApi; config: TuiConfig.Info }) { - const cwd = process.cwd() - if (loaded) { - if (dir !== cwd) { - throw new Error(`TuiPluginRuntime.init() called with a different working directory. expected=${dir} got=${cwd}`) - } - return loaded +export async function init(input: { api: HostPluginApi; config: TuiConfig.Info }) { + const cwd = process.cwd() + if (loaded) { + if (dir !== cwd) { + throw new Error(`TuiPluginRuntime.init() called with a different working directory. expected=${dir} got=${cwd}`) } - - dir = cwd - loaded = load(input) return loaded } - export function list() { - if (!runtime) return [] - return listPluginStatus(runtime) - } + dir = cwd + loaded = load(input) + return loaded +} - export async function activatePlugin(id: string) { - return activatePluginById(runtime, id, true) - } +export function list() { + if (!runtime) return [] + return listPluginStatus(runtime) +} - export async function deactivatePlugin(id: string) { - return deactivatePluginById(runtime, id, true) - } +export async function activatePlugin(id: string) { + return activatePluginById(runtime, id, true) +} - export async function addPlugin(spec: string) { - return addPluginBySpec(runtime, spec) - } +export async function deactivatePlugin(id: string) { + return deactivatePluginById(runtime, id, true) +} - export async function installPlugin(spec: string, options?: { global?: boolean }) { - return installPluginBySpec(runtime, spec, options?.global) - } +export async function addPlugin(spec: string) { + return addPluginBySpec(runtime, spec) +} - export async function dispose() { - const task = loaded - loaded = undefined - dir = "" - if (task) await task - const state = runtime - runtime = undefined - if (!state) return - const queue = [...state.plugins].reverse() - for (const plugin of queue) { - await deactivatePluginEntry(state, plugin, false) - } - } +export async function installPlugin(spec: string, options?: { global?: boolean }) { + return installPluginBySpec(runtime, spec, options?.global) +} - async function load(input: { api: Api; config: TuiConfig.Info }) { - const { api, config } = input - const cwd = process.cwd() - const slots = setupSlots(api) - const next: RuntimeState = { - directory: cwd, - api, - slots, - plugins: [], - plugins_by_id: new Map(), - pending: new Map(), - } - runtime = next - try { - await Instance.provide({ - directory: cwd, - fn: async () => { - const records = Flag.OPENCODE_PURE ? [] : (config.plugin_origins ?? []) - if (Flag.OPENCODE_PURE && config.plugin_origins?.length) { - log.info("skipping external tui plugins in pure mode", { count: config.plugin_origins.length }) - } - - for (const item of INTERNAL_TUI_PLUGINS) { - log.info("loading internal tui plugin", { id: item.id }) - const entry = loadInternalPlugin(item) - const meta = createMeta(entry.source, entry.spec, entry.target, undefined, entry.id) - addPluginEntry(next, { - id: entry.id, - load: entry, - meta, - themes: {}, - plugin: entry.module.tui, - enabled: true, - }) - } - - const ready = await resolveExternalPlugins(records, () => TuiConfig.waitForDependencies()) - await addExternalPluginEntries(next, ready) - - applyInitialPluginEnabledState(next, config) - for (const plugin of next.plugins) { - if (!plugin.enabled) continue - // Keep plugin execution sequential for deterministic side effects: - // command registration order affects keybind/command precedence, - // route registration is last-wins when ids collide, - // and hook chains rely on stable plugin ordering. - await activatePluginEntry(next, plugin, false) - } - }, - }) - } catch (error) { - fail("failed to load tui plugins", { directory: cwd, error }) - } +export async function dispose() { + const task = loaded + loaded = undefined + dir = "" + if (task) await task + const state = runtime + runtime = undefined + if (!state) return + const queue = [...state.plugins].reverse() + for (const plugin of queue) { + await deactivatePluginEntry(state, plugin, false) } } + +async function load(input: { api: Api; config: TuiConfig.Info }) { + const { api, config } = input + const cwd = process.cwd() + const slots = setupSlots(api) + const next: RuntimeState = { + directory: cwd, + api, + slots, + plugins: [], + plugins_by_id: new Map(), + pending: new Map(), + } + runtime = next + try { + await Instance.provide({ + directory: cwd, + fn: async () => { + const records = Flag.OPENCODE_PURE ? [] : (config.plugin_origins ?? []) + if (Flag.OPENCODE_PURE && config.plugin_origins?.length) { + log.info("skipping external tui plugins in pure mode", { count: config.plugin_origins.length }) + } + + for (const item of INTERNAL_TUI_PLUGINS) { + log.info("loading internal tui plugin", { id: item.id }) + const entry = loadInternalPlugin(item) + const meta = createMeta(entry.source, entry.spec, entry.target, undefined, entry.id) + addPluginEntry(next, { + id: entry.id, + load: entry, + meta, + themes: {}, + plugin: entry.module.tui, + enabled: true, + }) + } + + const ready = await resolveExternalPlugins(records, () => TuiConfig.waitForDependencies()) + await addExternalPluginEntries(next, ready) + + applyInitialPluginEnabledState(next, config) + for (const plugin of next.plugins) { + if (!plugin.enabled) continue + // Keep plugin execution sequential for deterministic side effects: + // command registration order affects keybind/command precedence, + // route registration is last-wins when ids collide, + // and hook chains rely on stable plugin ordering. + await activatePluginEntry(next, plugin, false) + } + }, + }) + } catch (error) { + fail("failed to load tui plugins", { directory: cwd, error }) + } +} + +export * as TuiPluginRuntime from "./runtime" From c59df636cc3d9b203e2b84dcefecba15eda5b457 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 21:02:09 -0400 Subject: [PATCH 040/335] chore: delete empty v2/session-common + collapse patch barrel (#22981) --- packages/opencode/src/patch/index.ts | 681 ++++++++++++++++++++- packages/opencode/src/patch/patch.ts | 678 -------------------- packages/opencode/src/v2/session-common.ts | 1 - 3 files changed, 680 insertions(+), 680 deletions(-) delete mode 100644 packages/opencode/src/patch/patch.ts delete mode 100644 packages/opencode/src/v2/session-common.ts diff --git a/packages/opencode/src/patch/index.ts b/packages/opencode/src/patch/index.ts index cec24614d8..19e1d7555b 100644 --- a/packages/opencode/src/patch/index.ts +++ b/packages/opencode/src/patch/index.ts @@ -1 +1,680 @@ -export * as Patch from "./patch" +import z from "zod" +import * as path from "path" +import * as fs from "fs/promises" +import { readFileSync } from "fs" +import { Log } from "../util" + +const log = Log.create({ service: "patch" }) + +// Schema definitions +export const PatchSchema = z.object({ + patchText: z.string().describe("The full patch text that describes all changes to be made"), +}) + +export type PatchParams = z.infer + +// Core types matching the Rust implementation +export interface ApplyPatchArgs { + patch: string + hunks: Hunk[] + workdir?: string +} + +export type Hunk = + | { type: "add"; path: string; contents: string } + | { type: "delete"; path: string } + | { type: "update"; path: string; move_path?: string; chunks: UpdateFileChunk[] } + +export interface UpdateFileChunk { + old_lines: string[] + new_lines: string[] + change_context?: string + is_end_of_file?: boolean +} + +export interface ApplyPatchAction { + changes: Map + patch: string + cwd: string +} + +export type ApplyPatchFileChange = + | { type: "add"; content: string } + | { type: "delete"; content: string } + | { type: "update"; unified_diff: string; move_path?: string; new_content: string } + +export interface AffectedPaths { + added: string[] + modified: string[] + deleted: string[] +} + +export enum ApplyPatchError { + ParseError = "ParseError", + IoError = "IoError", + ComputeReplacements = "ComputeReplacements", + ImplicitInvocation = "ImplicitInvocation", +} + +export enum MaybeApplyPatch { + Body = "Body", + ShellParseError = "ShellParseError", + PatchParseError = "PatchParseError", + NotApplyPatch = "NotApplyPatch", +} + +export enum MaybeApplyPatchVerified { + Body = "Body", + ShellParseError = "ShellParseError", + CorrectnessError = "CorrectnessError", + NotApplyPatch = "NotApplyPatch", +} + +// Parser implementation +function parsePatchHeader( + lines: string[], + startIdx: number, +): { filePath: string; movePath?: string; nextIdx: number } | null { + const line = lines[startIdx] + + if (line.startsWith("*** Add File:")) { + const filePath = line.slice("*** Add File:".length).trim() + return filePath ? { filePath, nextIdx: startIdx + 1 } : null + } + + if (line.startsWith("*** Delete File:")) { + const filePath = line.slice("*** Delete File:".length).trim() + return filePath ? { filePath, nextIdx: startIdx + 1 } : null + } + + if (line.startsWith("*** Update File:")) { + const filePath = line.slice("*** Update File:".length).trim() + let movePath: string | undefined + let nextIdx = startIdx + 1 + + // Check for move directive + if (nextIdx < lines.length && lines[nextIdx].startsWith("*** Move to:")) { + movePath = lines[nextIdx].slice("*** Move to:".length).trim() + nextIdx++ + } + + return filePath ? { filePath, movePath, nextIdx } : null + } + + return null +} + +function parseUpdateFileChunks(lines: string[], startIdx: number): { chunks: UpdateFileChunk[]; nextIdx: number } { + const chunks: UpdateFileChunk[] = [] + let i = startIdx + + while (i < lines.length && !lines[i].startsWith("***")) { + if (lines[i].startsWith("@@")) { + // Parse context line + const contextLine = lines[i].substring(2).trim() + i++ + + const oldLines: string[] = [] + const newLines: string[] = [] + let isEndOfFile = false + + // Parse change lines + while (i < lines.length && !lines[i].startsWith("@@") && !lines[i].startsWith("***")) { + const changeLine = lines[i] + + if (changeLine === "*** End of File") { + isEndOfFile = true + i++ + break + } + + if (changeLine.startsWith(" ")) { + // Keep line - appears in both old and new + const content = changeLine.substring(1) + oldLines.push(content) + newLines.push(content) + } else if (changeLine.startsWith("-")) { + // Remove line - only in old + oldLines.push(changeLine.substring(1)) + } else if (changeLine.startsWith("+")) { + // Add line - only in new + newLines.push(changeLine.substring(1)) + } + + i++ + } + + chunks.push({ + old_lines: oldLines, + new_lines: newLines, + change_context: contextLine || undefined, + is_end_of_file: isEndOfFile || undefined, + }) + } else { + i++ + } + } + + return { chunks, nextIdx: i } +} + +function parseAddFileContent(lines: string[], startIdx: number): { content: string; nextIdx: number } { + let content = "" + let i = startIdx + + while (i < lines.length && !lines[i].startsWith("***")) { + if (lines[i].startsWith("+")) { + content += lines[i].substring(1) + "\n" + } + i++ + } + + // Remove trailing newline + if (content.endsWith("\n")) { + content = content.slice(0, -1) + } + + return { content, nextIdx: i } +} + +function stripHeredoc(input: string): string { + // Match heredoc patterns like: cat <<'EOF'\n...\nEOF or < line.trim() === beginMarker) + const endIdx = lines.findIndex((line) => line.trim() === endMarker) + + if (beginIdx === -1 || endIdx === -1 || beginIdx >= endIdx) { + throw new Error("Invalid patch format: missing Begin/End markers") + } + + // Parse content between markers + i = beginIdx + 1 + + while (i < endIdx) { + const header = parsePatchHeader(lines, i) + if (!header) { + i++ + continue + } + + if (lines[i].startsWith("*** Add File:")) { + const { content, nextIdx } = parseAddFileContent(lines, header.nextIdx) + hunks.push({ + type: "add", + path: header.filePath, + contents: content, + }) + i = nextIdx + } else if (lines[i].startsWith("*** Delete File:")) { + hunks.push({ + type: "delete", + path: header.filePath, + }) + i = header.nextIdx + } else if (lines[i].startsWith("*** Update File:")) { + const { chunks, nextIdx } = parseUpdateFileChunks(lines, header.nextIdx) + hunks.push({ + type: "update", + path: header.filePath, + move_path: header.movePath, + chunks, + }) + i = nextIdx + } else { + i++ + } + } + + return { hunks } +} + +// Apply patch functionality +export function maybeParseApplyPatch( + argv: string[], +): + | { type: MaybeApplyPatch.Body; args: ApplyPatchArgs } + | { type: MaybeApplyPatch.PatchParseError; error: Error } + | { type: MaybeApplyPatch.NotApplyPatch } { + const APPLY_PATCH_COMMANDS = ["apply_patch", "applypatch"] + + // Direct invocation: apply_patch + if (argv.length === 2 && APPLY_PATCH_COMMANDS.includes(argv[0])) { + try { + const { hunks } = parsePatch(argv[1]) + return { + type: MaybeApplyPatch.Body, + args: { + patch: argv[1], + hunks, + }, + } + } catch (error) { + return { + type: MaybeApplyPatch.PatchParseError, + error: error as Error, + } + } + } + + // Bash heredoc form: bash -lc 'apply_patch <<"EOF" ...' + if (argv.length === 3 && argv[0] === "bash" && argv[1] === "-lc") { + // Simple extraction - in real implementation would need proper bash parsing + const script = argv[2] + const heredocMatch = script.match(/apply_patch\s*<<['"](\w+)['"]\s*\n([\s\S]*?)\n\1/) + + if (heredocMatch) { + const patchContent = heredocMatch[2] + try { + const { hunks } = parsePatch(patchContent) + return { + type: MaybeApplyPatch.Body, + args: { + patch: patchContent, + hunks, + }, + } + } catch (error) { + return { + type: MaybeApplyPatch.PatchParseError, + error: error as Error, + } + } + } + } + + return { type: MaybeApplyPatch.NotApplyPatch } +} + +// File content manipulation +interface ApplyPatchFileUpdate { + unified_diff: string + content: string +} + +export function deriveNewContentsFromChunks(filePath: string, chunks: UpdateFileChunk[]): ApplyPatchFileUpdate { + // Read original file content + let originalContent: string + try { + originalContent = readFileSync(filePath, "utf-8") + } catch (error) { + throw new Error(`Failed to read file ${filePath}: ${error}`, { cause: error }) + } + + let originalLines = originalContent.split("\n") + + // Drop trailing empty element for consistent line counting + if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") { + originalLines.pop() + } + + const replacements = computeReplacements(originalLines, filePath, chunks) + let newLines = applyReplacements(originalLines, replacements) + + // Ensure trailing newline + if (newLines.length === 0 || newLines[newLines.length - 1] !== "") { + newLines.push("") + } + + const newContent = newLines.join("\n") + + // Generate unified diff + const unifiedDiff = generateUnifiedDiff(originalContent, newContent) + + return { + unified_diff: unifiedDiff, + content: newContent, + } +} + +function computeReplacements( + originalLines: string[], + filePath: string, + chunks: UpdateFileChunk[], +): Array<[number, number, string[]]> { + const replacements: Array<[number, number, string[]]> = [] + let lineIndex = 0 + + for (const chunk of chunks) { + // Handle context-based seeking + if (chunk.change_context) { + const contextIdx = seekSequence(originalLines, [chunk.change_context], lineIndex) + if (contextIdx === -1) { + throw new Error(`Failed to find context '${chunk.change_context}' in ${filePath}`) + } + lineIndex = contextIdx + 1 + } + + // Handle pure addition (no old lines) + if (chunk.old_lines.length === 0) { + const insertionIdx = + originalLines.length > 0 && originalLines[originalLines.length - 1] === "" + ? originalLines.length - 1 + : originalLines.length + replacements.push([insertionIdx, 0, chunk.new_lines]) + continue + } + + // Try to match old lines in the file + let pattern = chunk.old_lines + let newSlice = chunk.new_lines + let found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file) + + // Retry without trailing empty line if not found + if (found === -1 && pattern.length > 0 && pattern[pattern.length - 1] === "") { + pattern = pattern.slice(0, -1) + if (newSlice.length > 0 && newSlice[newSlice.length - 1] === "") { + newSlice = newSlice.slice(0, -1) + } + found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file) + } + + if (found !== -1) { + replacements.push([found, pattern.length, newSlice]) + lineIndex = found + pattern.length + } else { + throw new Error(`Failed to find expected lines in ${filePath}:\n${chunk.old_lines.join("\n")}`) + } + } + + // Sort replacements by index to apply in order + replacements.sort((a, b) => a[0] - b[0]) + + return replacements +} + +function applyReplacements(lines: string[], replacements: Array<[number, number, string[]]>): string[] { + // Apply replacements in reverse order to avoid index shifting + const result = [...lines] + + for (let i = replacements.length - 1; i >= 0; i--) { + const [startIdx, oldLen, newSegment] = replacements[i] + + // Remove old lines + result.splice(startIdx, oldLen) + + // Insert new lines + for (let j = 0; j < newSegment.length; j++) { + result.splice(startIdx + j, 0, newSegment[j]) + } + } + + return result +} + +// Normalize Unicode punctuation to ASCII equivalents (like Rust's normalize_unicode) +function normalizeUnicode(str: string): string { + return str + .replace(/[\u2018\u2019\u201A\u201B]/g, "'") // single quotes + .replace(/[\u201C\u201D\u201E\u201F]/g, '"') // double quotes + .replace(/[\u2010\u2011\u2012\u2013\u2014\u2015]/g, "-") // dashes + .replace(/\u2026/g, "...") // ellipsis + .replace(/\u00A0/g, " ") // non-breaking space +} + +type Comparator = (a: string, b: string) => boolean + +function tryMatch(lines: string[], pattern: string[], startIndex: number, compare: Comparator, eof: boolean): number { + // If EOF anchor, try matching from end of file first + if (eof) { + const fromEnd = lines.length - pattern.length + if (fromEnd >= startIndex) { + let matches = true + for (let j = 0; j < pattern.length; j++) { + if (!compare(lines[fromEnd + j], pattern[j])) { + matches = false + break + } + } + if (matches) return fromEnd + } + } + + // Forward search from startIndex + for (let i = startIndex; i <= lines.length - pattern.length; i++) { + let matches = true + for (let j = 0; j < pattern.length; j++) { + if (!compare(lines[i + j], pattern[j])) { + matches = false + break + } + } + if (matches) return i + } + + return -1 +} + +function seekSequence(lines: string[], pattern: string[], startIndex: number, eof = false): number { + if (pattern.length === 0) return -1 + + // Pass 1: exact match + const exact = tryMatch(lines, pattern, startIndex, (a, b) => a === b, eof) + if (exact !== -1) return exact + + // Pass 2: rstrip (trim trailing whitespace) + const rstrip = tryMatch(lines, pattern, startIndex, (a, b) => a.trimEnd() === b.trimEnd(), eof) + if (rstrip !== -1) return rstrip + + // Pass 3: trim (both ends) + const trim = tryMatch(lines, pattern, startIndex, (a, b) => a.trim() === b.trim(), eof) + if (trim !== -1) return trim + + // Pass 4: normalized (Unicode punctuation to ASCII) + const normalized = tryMatch( + lines, + pattern, + startIndex, + (a, b) => normalizeUnicode(a.trim()) === normalizeUnicode(b.trim()), + eof, + ) + return normalized +} + +function generateUnifiedDiff(oldContent: string, newContent: string): string { + const oldLines = oldContent.split("\n") + const newLines = newContent.split("\n") + + // Simple diff generation - in a real implementation you'd use a proper diff algorithm + let diff = "@@ -1 +1 @@\n" + + // Find changes (simplified approach) + const maxLen = Math.max(oldLines.length, newLines.length) + let hasChanges = false + + for (let i = 0; i < maxLen; i++) { + const oldLine = oldLines[i] || "" + const newLine = newLines[i] || "" + + if (oldLine !== newLine) { + if (oldLine) diff += `-${oldLine}\n` + if (newLine) diff += `+${newLine}\n` + hasChanges = true + } else if (oldLine) { + diff += ` ${oldLine}\n` + } + } + + return hasChanges ? diff : "" +} + +// Apply hunks to filesystem +export async function applyHunksToFiles(hunks: Hunk[]): Promise { + if (hunks.length === 0) { + throw new Error("No files were modified.") + } + + const added: string[] = [] + const modified: string[] = [] + const deleted: string[] = [] + + for (const hunk of hunks) { + switch (hunk.type) { + case "add": + // Create parent directories + const addDir = path.dirname(hunk.path) + if (addDir !== "." && addDir !== "/") { + await fs.mkdir(addDir, { recursive: true }) + } + + await fs.writeFile(hunk.path, hunk.contents, "utf-8") + added.push(hunk.path) + log.info(`Added file: ${hunk.path}`) + break + + case "delete": + await fs.unlink(hunk.path) + deleted.push(hunk.path) + log.info(`Deleted file: ${hunk.path}`) + break + + case "update": + const fileUpdate = deriveNewContentsFromChunks(hunk.path, hunk.chunks) + + if (hunk.move_path) { + // Handle file move + const moveDir = path.dirname(hunk.move_path) + if (moveDir !== "." && moveDir !== "/") { + await fs.mkdir(moveDir, { recursive: true }) + } + + await fs.writeFile(hunk.move_path, fileUpdate.content, "utf-8") + await fs.unlink(hunk.path) + modified.push(hunk.move_path) + log.info(`Moved file: ${hunk.path} -> ${hunk.move_path}`) + } else { + // Regular update + await fs.writeFile(hunk.path, fileUpdate.content, "utf-8") + modified.push(hunk.path) + log.info(`Updated file: ${hunk.path}`) + } + break + } + } + + return { added, modified, deleted } +} + +// Main patch application function +export async function applyPatch(patchText: string): Promise { + const { hunks } = parsePatch(patchText) + return applyHunksToFiles(hunks) +} + +// Async version of maybeParseApplyPatchVerified +export async function maybeParseApplyPatchVerified( + argv: string[], + cwd: string, +): Promise< + | { type: MaybeApplyPatchVerified.Body; action: ApplyPatchAction } + | { type: MaybeApplyPatchVerified.CorrectnessError; error: Error } + | { type: MaybeApplyPatchVerified.NotApplyPatch } +> { + // Detect implicit patch invocation (raw patch without apply_patch command) + if (argv.length === 1) { + try { + parsePatch(argv[0]) + return { + type: MaybeApplyPatchVerified.CorrectnessError, + error: new Error(ApplyPatchError.ImplicitInvocation), + } + } catch { + // Not a patch, continue + } + } + + const result = maybeParseApplyPatch(argv) + + switch (result.type) { + case MaybeApplyPatch.Body: + const { args } = result + const effectiveCwd = args.workdir ? path.resolve(cwd, args.workdir) : cwd + const changes = new Map() + + for (const hunk of args.hunks) { + const resolvedPath = path.resolve( + effectiveCwd, + hunk.type === "update" && hunk.move_path ? hunk.move_path : hunk.path, + ) + + switch (hunk.type) { + case "add": + changes.set(resolvedPath, { + type: "add", + content: hunk.contents, + }) + break + + case "delete": + // For delete, we need to read the current content + const deletePath = path.resolve(effectiveCwd, hunk.path) + try { + const content = await fs.readFile(deletePath, "utf-8") + changes.set(resolvedPath, { + type: "delete", + content, + }) + } catch { + return { + type: MaybeApplyPatchVerified.CorrectnessError, + error: new Error(`Failed to read file for deletion: ${deletePath}`), + } + } + break + + case "update": + const updatePath = path.resolve(effectiveCwd, hunk.path) + try { + const fileUpdate = deriveNewContentsFromChunks(updatePath, hunk.chunks) + changes.set(resolvedPath, { + type: "update", + unified_diff: fileUpdate.unified_diff, + move_path: hunk.move_path ? path.resolve(effectiveCwd, hunk.move_path) : undefined, + new_content: fileUpdate.content, + }) + } catch (error) { + return { + type: MaybeApplyPatchVerified.CorrectnessError, + error: error as Error, + } + } + break + } + } + + return { + type: MaybeApplyPatchVerified.Body, + action: { + changes, + patch: args.patch, + cwd: effectiveCwd, + }, + } + + case MaybeApplyPatch.PatchParseError: + return { + type: MaybeApplyPatchVerified.CorrectnessError, + error: result.error, + } + + case MaybeApplyPatch.NotApplyPatch: + return { type: MaybeApplyPatchVerified.NotApplyPatch } + } +} + +export * as Patch from "." diff --git a/packages/opencode/src/patch/patch.ts b/packages/opencode/src/patch/patch.ts deleted file mode 100644 index 1dc99b4da9..0000000000 --- a/packages/opencode/src/patch/patch.ts +++ /dev/null @@ -1,678 +0,0 @@ -import z from "zod" -import * as path from "path" -import * as fs from "fs/promises" -import { readFileSync } from "fs" -import { Log } from "../util" - -const log = Log.create({ service: "patch" }) - -// Schema definitions -export const PatchSchema = z.object({ - patchText: z.string().describe("The full patch text that describes all changes to be made"), -}) - -export type PatchParams = z.infer - -// Core types matching the Rust implementation -export interface ApplyPatchArgs { - patch: string - hunks: Hunk[] - workdir?: string -} - -export type Hunk = - | { type: "add"; path: string; contents: string } - | { type: "delete"; path: string } - | { type: "update"; path: string; move_path?: string; chunks: UpdateFileChunk[] } - -export interface UpdateFileChunk { - old_lines: string[] - new_lines: string[] - change_context?: string - is_end_of_file?: boolean -} - -export interface ApplyPatchAction { - changes: Map - patch: string - cwd: string -} - -export type ApplyPatchFileChange = - | { type: "add"; content: string } - | { type: "delete"; content: string } - | { type: "update"; unified_diff: string; move_path?: string; new_content: string } - -export interface AffectedPaths { - added: string[] - modified: string[] - deleted: string[] -} - -export enum ApplyPatchError { - ParseError = "ParseError", - IoError = "IoError", - ComputeReplacements = "ComputeReplacements", - ImplicitInvocation = "ImplicitInvocation", -} - -export enum MaybeApplyPatch { - Body = "Body", - ShellParseError = "ShellParseError", - PatchParseError = "PatchParseError", - NotApplyPatch = "NotApplyPatch", -} - -export enum MaybeApplyPatchVerified { - Body = "Body", - ShellParseError = "ShellParseError", - CorrectnessError = "CorrectnessError", - NotApplyPatch = "NotApplyPatch", -} - -// Parser implementation -function parsePatchHeader( - lines: string[], - startIdx: number, -): { filePath: string; movePath?: string; nextIdx: number } | null { - const line = lines[startIdx] - - if (line.startsWith("*** Add File:")) { - const filePath = line.slice("*** Add File:".length).trim() - return filePath ? { filePath, nextIdx: startIdx + 1 } : null - } - - if (line.startsWith("*** Delete File:")) { - const filePath = line.slice("*** Delete File:".length).trim() - return filePath ? { filePath, nextIdx: startIdx + 1 } : null - } - - if (line.startsWith("*** Update File:")) { - const filePath = line.slice("*** Update File:".length).trim() - let movePath: string | undefined - let nextIdx = startIdx + 1 - - // Check for move directive - if (nextIdx < lines.length && lines[nextIdx].startsWith("*** Move to:")) { - movePath = lines[nextIdx].slice("*** Move to:".length).trim() - nextIdx++ - } - - return filePath ? { filePath, movePath, nextIdx } : null - } - - return null -} - -function parseUpdateFileChunks(lines: string[], startIdx: number): { chunks: UpdateFileChunk[]; nextIdx: number } { - const chunks: UpdateFileChunk[] = [] - let i = startIdx - - while (i < lines.length && !lines[i].startsWith("***")) { - if (lines[i].startsWith("@@")) { - // Parse context line - const contextLine = lines[i].substring(2).trim() - i++ - - const oldLines: string[] = [] - const newLines: string[] = [] - let isEndOfFile = false - - // Parse change lines - while (i < lines.length && !lines[i].startsWith("@@") && !lines[i].startsWith("***")) { - const changeLine = lines[i] - - if (changeLine === "*** End of File") { - isEndOfFile = true - i++ - break - } - - if (changeLine.startsWith(" ")) { - // Keep line - appears in both old and new - const content = changeLine.substring(1) - oldLines.push(content) - newLines.push(content) - } else if (changeLine.startsWith("-")) { - // Remove line - only in old - oldLines.push(changeLine.substring(1)) - } else if (changeLine.startsWith("+")) { - // Add line - only in new - newLines.push(changeLine.substring(1)) - } - - i++ - } - - chunks.push({ - old_lines: oldLines, - new_lines: newLines, - change_context: contextLine || undefined, - is_end_of_file: isEndOfFile || undefined, - }) - } else { - i++ - } - } - - return { chunks, nextIdx: i } -} - -function parseAddFileContent(lines: string[], startIdx: number): { content: string; nextIdx: number } { - let content = "" - let i = startIdx - - while (i < lines.length && !lines[i].startsWith("***")) { - if (lines[i].startsWith("+")) { - content += lines[i].substring(1) + "\n" - } - i++ - } - - // Remove trailing newline - if (content.endsWith("\n")) { - content = content.slice(0, -1) - } - - return { content, nextIdx: i } -} - -function stripHeredoc(input: string): string { - // Match heredoc patterns like: cat <<'EOF'\n...\nEOF or < line.trim() === beginMarker) - const endIdx = lines.findIndex((line) => line.trim() === endMarker) - - if (beginIdx === -1 || endIdx === -1 || beginIdx >= endIdx) { - throw new Error("Invalid patch format: missing Begin/End markers") - } - - // Parse content between markers - i = beginIdx + 1 - - while (i < endIdx) { - const header = parsePatchHeader(lines, i) - if (!header) { - i++ - continue - } - - if (lines[i].startsWith("*** Add File:")) { - const { content, nextIdx } = parseAddFileContent(lines, header.nextIdx) - hunks.push({ - type: "add", - path: header.filePath, - contents: content, - }) - i = nextIdx - } else if (lines[i].startsWith("*** Delete File:")) { - hunks.push({ - type: "delete", - path: header.filePath, - }) - i = header.nextIdx - } else if (lines[i].startsWith("*** Update File:")) { - const { chunks, nextIdx } = parseUpdateFileChunks(lines, header.nextIdx) - hunks.push({ - type: "update", - path: header.filePath, - move_path: header.movePath, - chunks, - }) - i = nextIdx - } else { - i++ - } - } - - return { hunks } -} - -// Apply patch functionality -export function maybeParseApplyPatch( - argv: string[], -): - | { type: MaybeApplyPatch.Body; args: ApplyPatchArgs } - | { type: MaybeApplyPatch.PatchParseError; error: Error } - | { type: MaybeApplyPatch.NotApplyPatch } { - const APPLY_PATCH_COMMANDS = ["apply_patch", "applypatch"] - - // Direct invocation: apply_patch - if (argv.length === 2 && APPLY_PATCH_COMMANDS.includes(argv[0])) { - try { - const { hunks } = parsePatch(argv[1]) - return { - type: MaybeApplyPatch.Body, - args: { - patch: argv[1], - hunks, - }, - } - } catch (error) { - return { - type: MaybeApplyPatch.PatchParseError, - error: error as Error, - } - } - } - - // Bash heredoc form: bash -lc 'apply_patch <<"EOF" ...' - if (argv.length === 3 && argv[0] === "bash" && argv[1] === "-lc") { - // Simple extraction - in real implementation would need proper bash parsing - const script = argv[2] - const heredocMatch = script.match(/apply_patch\s*<<['"](\w+)['"]\s*\n([\s\S]*?)\n\1/) - - if (heredocMatch) { - const patchContent = heredocMatch[2] - try { - const { hunks } = parsePatch(patchContent) - return { - type: MaybeApplyPatch.Body, - args: { - patch: patchContent, - hunks, - }, - } - } catch (error) { - return { - type: MaybeApplyPatch.PatchParseError, - error: error as Error, - } - } - } - } - - return { type: MaybeApplyPatch.NotApplyPatch } -} - -// File content manipulation -interface ApplyPatchFileUpdate { - unified_diff: string - content: string -} - -export function deriveNewContentsFromChunks(filePath: string, chunks: UpdateFileChunk[]): ApplyPatchFileUpdate { - // Read original file content - let originalContent: string - try { - originalContent = readFileSync(filePath, "utf-8") - } catch (error) { - throw new Error(`Failed to read file ${filePath}: ${error}`, { cause: error }) - } - - let originalLines = originalContent.split("\n") - - // Drop trailing empty element for consistent line counting - if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") { - originalLines.pop() - } - - const replacements = computeReplacements(originalLines, filePath, chunks) - let newLines = applyReplacements(originalLines, replacements) - - // Ensure trailing newline - if (newLines.length === 0 || newLines[newLines.length - 1] !== "") { - newLines.push("") - } - - const newContent = newLines.join("\n") - - // Generate unified diff - const unifiedDiff = generateUnifiedDiff(originalContent, newContent) - - return { - unified_diff: unifiedDiff, - content: newContent, - } -} - -function computeReplacements( - originalLines: string[], - filePath: string, - chunks: UpdateFileChunk[], -): Array<[number, number, string[]]> { - const replacements: Array<[number, number, string[]]> = [] - let lineIndex = 0 - - for (const chunk of chunks) { - // Handle context-based seeking - if (chunk.change_context) { - const contextIdx = seekSequence(originalLines, [chunk.change_context], lineIndex) - if (contextIdx === -1) { - throw new Error(`Failed to find context '${chunk.change_context}' in ${filePath}`) - } - lineIndex = contextIdx + 1 - } - - // Handle pure addition (no old lines) - if (chunk.old_lines.length === 0) { - const insertionIdx = - originalLines.length > 0 && originalLines[originalLines.length - 1] === "" - ? originalLines.length - 1 - : originalLines.length - replacements.push([insertionIdx, 0, chunk.new_lines]) - continue - } - - // Try to match old lines in the file - let pattern = chunk.old_lines - let newSlice = chunk.new_lines - let found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file) - - // Retry without trailing empty line if not found - if (found === -1 && pattern.length > 0 && pattern[pattern.length - 1] === "") { - pattern = pattern.slice(0, -1) - if (newSlice.length > 0 && newSlice[newSlice.length - 1] === "") { - newSlice = newSlice.slice(0, -1) - } - found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file) - } - - if (found !== -1) { - replacements.push([found, pattern.length, newSlice]) - lineIndex = found + pattern.length - } else { - throw new Error(`Failed to find expected lines in ${filePath}:\n${chunk.old_lines.join("\n")}`) - } - } - - // Sort replacements by index to apply in order - replacements.sort((a, b) => a[0] - b[0]) - - return replacements -} - -function applyReplacements(lines: string[], replacements: Array<[number, number, string[]]>): string[] { - // Apply replacements in reverse order to avoid index shifting - const result = [...lines] - - for (let i = replacements.length - 1; i >= 0; i--) { - const [startIdx, oldLen, newSegment] = replacements[i] - - // Remove old lines - result.splice(startIdx, oldLen) - - // Insert new lines - for (let j = 0; j < newSegment.length; j++) { - result.splice(startIdx + j, 0, newSegment[j]) - } - } - - return result -} - -// Normalize Unicode punctuation to ASCII equivalents (like Rust's normalize_unicode) -function normalizeUnicode(str: string): string { - return str - .replace(/[\u2018\u2019\u201A\u201B]/g, "'") // single quotes - .replace(/[\u201C\u201D\u201E\u201F]/g, '"') // double quotes - .replace(/[\u2010\u2011\u2012\u2013\u2014\u2015]/g, "-") // dashes - .replace(/\u2026/g, "...") // ellipsis - .replace(/\u00A0/g, " ") // non-breaking space -} - -type Comparator = (a: string, b: string) => boolean - -function tryMatch(lines: string[], pattern: string[], startIndex: number, compare: Comparator, eof: boolean): number { - // If EOF anchor, try matching from end of file first - if (eof) { - const fromEnd = lines.length - pattern.length - if (fromEnd >= startIndex) { - let matches = true - for (let j = 0; j < pattern.length; j++) { - if (!compare(lines[fromEnd + j], pattern[j])) { - matches = false - break - } - } - if (matches) return fromEnd - } - } - - // Forward search from startIndex - for (let i = startIndex; i <= lines.length - pattern.length; i++) { - let matches = true - for (let j = 0; j < pattern.length; j++) { - if (!compare(lines[i + j], pattern[j])) { - matches = false - break - } - } - if (matches) return i - } - - return -1 -} - -function seekSequence(lines: string[], pattern: string[], startIndex: number, eof = false): number { - if (pattern.length === 0) return -1 - - // Pass 1: exact match - const exact = tryMatch(lines, pattern, startIndex, (a, b) => a === b, eof) - if (exact !== -1) return exact - - // Pass 2: rstrip (trim trailing whitespace) - const rstrip = tryMatch(lines, pattern, startIndex, (a, b) => a.trimEnd() === b.trimEnd(), eof) - if (rstrip !== -1) return rstrip - - // Pass 3: trim (both ends) - const trim = tryMatch(lines, pattern, startIndex, (a, b) => a.trim() === b.trim(), eof) - if (trim !== -1) return trim - - // Pass 4: normalized (Unicode punctuation to ASCII) - const normalized = tryMatch( - lines, - pattern, - startIndex, - (a, b) => normalizeUnicode(a.trim()) === normalizeUnicode(b.trim()), - eof, - ) - return normalized -} - -function generateUnifiedDiff(oldContent: string, newContent: string): string { - const oldLines = oldContent.split("\n") - const newLines = newContent.split("\n") - - // Simple diff generation - in a real implementation you'd use a proper diff algorithm - let diff = "@@ -1 +1 @@\n" - - // Find changes (simplified approach) - const maxLen = Math.max(oldLines.length, newLines.length) - let hasChanges = false - - for (let i = 0; i < maxLen; i++) { - const oldLine = oldLines[i] || "" - const newLine = newLines[i] || "" - - if (oldLine !== newLine) { - if (oldLine) diff += `-${oldLine}\n` - if (newLine) diff += `+${newLine}\n` - hasChanges = true - } else if (oldLine) { - diff += ` ${oldLine}\n` - } - } - - return hasChanges ? diff : "" -} - -// Apply hunks to filesystem -export async function applyHunksToFiles(hunks: Hunk[]): Promise { - if (hunks.length === 0) { - throw new Error("No files were modified.") - } - - const added: string[] = [] - const modified: string[] = [] - const deleted: string[] = [] - - for (const hunk of hunks) { - switch (hunk.type) { - case "add": - // Create parent directories - const addDir = path.dirname(hunk.path) - if (addDir !== "." && addDir !== "/") { - await fs.mkdir(addDir, { recursive: true }) - } - - await fs.writeFile(hunk.path, hunk.contents, "utf-8") - added.push(hunk.path) - log.info(`Added file: ${hunk.path}`) - break - - case "delete": - await fs.unlink(hunk.path) - deleted.push(hunk.path) - log.info(`Deleted file: ${hunk.path}`) - break - - case "update": - const fileUpdate = deriveNewContentsFromChunks(hunk.path, hunk.chunks) - - if (hunk.move_path) { - // Handle file move - const moveDir = path.dirname(hunk.move_path) - if (moveDir !== "." && moveDir !== "/") { - await fs.mkdir(moveDir, { recursive: true }) - } - - await fs.writeFile(hunk.move_path, fileUpdate.content, "utf-8") - await fs.unlink(hunk.path) - modified.push(hunk.move_path) - log.info(`Moved file: ${hunk.path} -> ${hunk.move_path}`) - } else { - // Regular update - await fs.writeFile(hunk.path, fileUpdate.content, "utf-8") - modified.push(hunk.path) - log.info(`Updated file: ${hunk.path}`) - } - break - } - } - - return { added, modified, deleted } -} - -// Main patch application function -export async function applyPatch(patchText: string): Promise { - const { hunks } = parsePatch(patchText) - return applyHunksToFiles(hunks) -} - -// Async version of maybeParseApplyPatchVerified -export async function maybeParseApplyPatchVerified( - argv: string[], - cwd: string, -): Promise< - | { type: MaybeApplyPatchVerified.Body; action: ApplyPatchAction } - | { type: MaybeApplyPatchVerified.CorrectnessError; error: Error } - | { type: MaybeApplyPatchVerified.NotApplyPatch } -> { - // Detect implicit patch invocation (raw patch without apply_patch command) - if (argv.length === 1) { - try { - parsePatch(argv[0]) - return { - type: MaybeApplyPatchVerified.CorrectnessError, - error: new Error(ApplyPatchError.ImplicitInvocation), - } - } catch { - // Not a patch, continue - } - } - - const result = maybeParseApplyPatch(argv) - - switch (result.type) { - case MaybeApplyPatch.Body: - const { args } = result - const effectiveCwd = args.workdir ? path.resolve(cwd, args.workdir) : cwd - const changes = new Map() - - for (const hunk of args.hunks) { - const resolvedPath = path.resolve( - effectiveCwd, - hunk.type === "update" && hunk.move_path ? hunk.move_path : hunk.path, - ) - - switch (hunk.type) { - case "add": - changes.set(resolvedPath, { - type: "add", - content: hunk.contents, - }) - break - - case "delete": - // For delete, we need to read the current content - const deletePath = path.resolve(effectiveCwd, hunk.path) - try { - const content = await fs.readFile(deletePath, "utf-8") - changes.set(resolvedPath, { - type: "delete", - content, - }) - } catch { - return { - type: MaybeApplyPatchVerified.CorrectnessError, - error: new Error(`Failed to read file for deletion: ${deletePath}`), - } - } - break - - case "update": - const updatePath = path.resolve(effectiveCwd, hunk.path) - try { - const fileUpdate = deriveNewContentsFromChunks(updatePath, hunk.chunks) - changes.set(resolvedPath, { - type: "update", - unified_diff: fileUpdate.unified_diff, - move_path: hunk.move_path ? path.resolve(effectiveCwd, hunk.move_path) : undefined, - new_content: fileUpdate.content, - }) - } catch (error) { - return { - type: MaybeApplyPatchVerified.CorrectnessError, - error: error as Error, - } - } - break - } - } - - return { - type: MaybeApplyPatchVerified.Body, - action: { - changes, - patch: args.patch, - cwd: effectiveCwd, - }, - } - - case MaybeApplyPatch.PatchParseError: - return { - type: MaybeApplyPatchVerified.CorrectnessError, - error: result.error, - } - - case MaybeApplyPatch.NotApplyPatch: - return { type: MaybeApplyPatchVerified.NotApplyPatch } - } -} diff --git a/packages/opencode/src/v2/session-common.ts b/packages/opencode/src/v2/session-common.ts deleted file mode 100644 index 556bd79b61..0000000000 --- a/packages/opencode/src/v2/session-common.ts +++ /dev/null @@ -1 +0,0 @@ -export namespace SessionCommon {} From 8afb625bab10c44e5b0437af4550f020f332cdf5 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 21:19:01 -0400 Subject: [PATCH 041/335] refactor: extract Diagnostic namespace into lsp/diagnostic.ts + self-reexport (#22983) --- packages/opencode/src/lsp/diagnostic.ts | 29 +++++++++++++++++++++++++ packages/opencode/src/lsp/lsp.ts | 28 +----------------------- 2 files changed, 30 insertions(+), 27 deletions(-) create mode 100644 packages/opencode/src/lsp/diagnostic.ts diff --git a/packages/opencode/src/lsp/diagnostic.ts b/packages/opencode/src/lsp/diagnostic.ts new file mode 100644 index 0000000000..4bc085e788 --- /dev/null +++ b/packages/opencode/src/lsp/diagnostic.ts @@ -0,0 +1,29 @@ +import * as LSPClient from "./client" + +const MAX_PER_FILE = 20 + +export function pretty(diagnostic: LSPClient.Diagnostic) { + const severityMap = { + 1: "ERROR", + 2: "WARN", + 3: "INFO", + 4: "HINT", + } + + const severity = severityMap[diagnostic.severity || 1] + const line = diagnostic.range.start.line + 1 + const col = diagnostic.range.start.character + 1 + + return `${severity} [${line}:${col}] ${diagnostic.message}` +} + +export function report(file: string, issues: LSPClient.Diagnostic[]) { + const errors = issues.filter((item) => item.severity === 1) + if (errors.length === 0) return "" + const limited = errors.slice(0, MAX_PER_FILE) + const more = errors.length - MAX_PER_FILE + const suffix = more > 0 ? `\n... and ${more} more` : "" + return `\n${limited.map(pretty).join("\n")}${suffix}\n` +} + +export * as Diagnostic from "./diagnostic" diff --git a/packages/opencode/src/lsp/lsp.ts b/packages/opencode/src/lsp/lsp.ts index d895e73256..97af8209bb 100644 --- a/packages/opencode/src/lsp/lsp.ts +++ b/packages/opencode/src/lsp/lsp.ts @@ -505,30 +505,4 @@ export const layer = Layer.effect( export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer)) -export namespace Diagnostic { - const MAX_PER_FILE = 20 - - export function pretty(diagnostic: LSPClient.Diagnostic) { - const severityMap = { - 1: "ERROR", - 2: "WARN", - 3: "INFO", - 4: "HINT", - } - - const severity = severityMap[diagnostic.severity || 1] - const line = diagnostic.range.start.line + 1 - const col = diagnostic.range.start.character + 1 - - return `${severity} [${line}:${col}] ${diagnostic.message}` - } - - export function report(file: string, issues: LSPClient.Diagnostic[]) { - const errors = issues.filter((item) => item.severity === 1) - if (errors.length === 0) return "" - const limited = errors.slice(0, MAX_PER_FILE) - const more = errors.length - MAX_PER_FILE - const suffix = more > 0 ? `\n... and ${more} more` : "" - return `\n${limited.map(pretty).join("\n")}${suffix}\n` - } -} +export * as Diagnostic from "./diagnostic" From 6405e3a7b12d49ade2f251360d221999836ccc02 Mon Sep 17 00:00:00 2001 From: Dax Date: Thu, 16 Apr 2026 21:32:36 -0400 Subject: [PATCH 042/335] tui: stabilize session dialog ordering (#22987) --- .../src/cli/cmd/tui/component/dialog-session-list.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 75c79dcdd8..60ef6087ba 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -113,7 +113,11 @@ export function DialogSessionList() { const today = new Date().toDateString() return sessions() .filter((x) => x.parentID === undefined) - .toSorted((a, b) => b.time.updated - a.time.updated) + .toSorted((a, b) => { + const updatedDay = new Date(b.time.updated).setHours(0, 0, 0, 0) - new Date(a.time.updated).setHours(0, 0, 0, 0) + if (updatedDay !== 0) return updatedDay + return b.time.created - a.time.created + }) .map((x) => { const workspace = x.workspaceID ? project.workspace.get(x.workspaceID) : undefined From 326471a25c50cb83d33e6b327bc88faf38a4db11 Mon Sep 17 00:00:00 2001 From: Dax Date: Thu, 16 Apr 2026 21:35:26 -0400 Subject: [PATCH 043/335] refactor: split config lsp and formatter schemas (#22986) --- AGENTS.md | 1 + packages/opencode/AGENTS.md | 4 ++ packages/opencode/src/config/config.ts | 55 ++--------------------- packages/opencode/src/config/formatter.ts | 13 ++++++ packages/opencode/src/config/index.ts | 2 + packages/opencode/src/config/lsp.ts | 39 ++++++++++++++++ 6 files changed, 63 insertions(+), 51 deletions(-) create mode 100644 packages/opencode/src/config/formatter.ts create mode 100644 packages/opencode/src/config/lsp.ts diff --git a/AGENTS.md b/AGENTS.md index a7895c831f..44d08ae955 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,7 @@ - Use Bun APIs when possible, like `Bun.file()` - Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity - Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream +- In `src/config`, follow the existing self-export pattern at the top of the file (for example `export * as ConfigAgent from "./agent"`) when adding a new config module. Reduce total variable count by inlining when a value is only used once. diff --git a/packages/opencode/AGENTS.md b/packages/opencode/AGENTS.md index f0f32fdd16..761b9b5c5e 100644 --- a/packages/opencode/AGENTS.md +++ b/packages/opencode/AGENTS.md @@ -23,6 +23,10 @@ See `specs/effect/migration.md` for the compact pattern reference and examples. - Use `Effect.callback` for callback-based APIs. - Prefer `DateTime.nowAsDate` over `new Date(yield* Clock.currentTimeMillis)` when you need a `Date`. +## Module conventions + +- In `src/config`, follow the existing self-export pattern at the top of the file (for example `export * as ConfigAgent from "./agent"`) when adding a new config module. + ## Schemas and errors - Use `Schema.Class` for multi-field data. diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index adccb6353b..2edc455df3 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -12,7 +12,6 @@ import { Auth } from "../auth" import { Env } from "../env" import { applyEdits, modify } from "jsonc-parser" import { Instance, type InstanceContext } from "../project/instance" -import * as LSPServer from "../lsp/server" import { InstallationLocal, InstallationVersion } from "@/installation/version" import { existsSync } from "fs" import { GlobalBus } from "@/bus/global" @@ -37,6 +36,8 @@ import { ConfigPermission } from "./permission" import { ConfigProvider } from "./provider" import { ConfigSkills } from "./skills" import { ConfigPaths } from "./paths" +import { ConfigFormatter } from "./formatter" +import { ConfigLSP } from "./lsp" const log = Log.create({ service: "config" }) @@ -186,56 +187,8 @@ export const Info = z ) .optional() .describe("MCP (Model Context Protocol) server configurations"), - formatter: z - .union([ - z.literal(false), - z.record( - z.string(), - z.object({ - disabled: z.boolean().optional(), - command: z.array(z.string()).optional(), - environment: z.record(z.string(), z.string()).optional(), - extensions: z.array(z.string()).optional(), - }), - ), - ]) - .optional(), - lsp: z - .union([ - z.literal(false), - z.record( - z.string(), - z.union([ - z.object({ - disabled: z.literal(true), - }), - z.object({ - command: z.array(z.string()), - extensions: z.array(z.string()).optional(), - disabled: z.boolean().optional(), - env: z.record(z.string(), z.string()).optional(), - initialization: z.record(z.string(), z.any()).optional(), - }), - ]), - ), - ]) - .optional() - .refine( - (data) => { - if (!data) return true - if (typeof data === "boolean") return true - const serverIds = new Set(Object.values(LSPServer).map((s) => s.id)) - - return Object.entries(data).every(([id, config]) => { - if (config.disabled) return true - if (serverIds.has(id)) return true - return Boolean(config.extensions) - }) - }, - { - error: "For custom LSP servers, 'extensions' array is required.", - }, - ), + formatter: ConfigFormatter.Info.optional(), + lsp: ConfigLSP.Info.optional(), instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"), layout: Layout.optional().describe("@deprecated Always uses stretch layout."), permission: ConfigPermission.Info.optional(), diff --git a/packages/opencode/src/config/formatter.ts b/packages/opencode/src/config/formatter.ts new file mode 100644 index 0000000000..7ac56214c9 --- /dev/null +++ b/packages/opencode/src/config/formatter.ts @@ -0,0 +1,13 @@ +export * as ConfigFormatter from "./formatter" + +import z from "zod" + +export const Entry = z.object({ + disabled: z.boolean().optional(), + command: z.array(z.string()).optional(), + environment: z.record(z.string(), z.string()).optional(), + extensions: z.array(z.string()).optional(), +}) + +export const Info = z.union([z.literal(false), z.record(z.string(), Entry)]) +export type Info = z.infer diff --git a/packages/opencode/src/config/index.ts b/packages/opencode/src/config/index.ts index c4a1c608b1..a05c29d25c 100644 --- a/packages/opencode/src/config/index.ts +++ b/packages/opencode/src/config/index.ts @@ -2,6 +2,8 @@ export * as Config from "./config" export * as ConfigAgent from "./agent" export * as ConfigCommand from "./command" export * as ConfigError from "./error" +export * as ConfigFormatter from "./formatter" +export * as ConfigLSP from "./lsp" export * as ConfigVariable from "./variable" export { ConfigManaged } from "./managed" export * as ConfigMarkdown from "./markdown" diff --git a/packages/opencode/src/config/lsp.ts b/packages/opencode/src/config/lsp.ts new file mode 100644 index 0000000000..afb83908b9 --- /dev/null +++ b/packages/opencode/src/config/lsp.ts @@ -0,0 +1,39 @@ +export * as ConfigLSP from "./lsp" + +import z from "zod" +import * as LSPServer from "../lsp/server" + +export const Disabled = z.object({ + disabled: z.literal(true), +}) + +export const Entry = z.union([ + Disabled, + z.object({ + command: z.array(z.string()), + extensions: z.array(z.string()).optional(), + disabled: z.boolean().optional(), + env: z.record(z.string(), z.string()).optional(), + initialization: z.record(z.string(), z.any()).optional(), + }), +]) + +export const Info = z + .union([z.literal(false), z.record(z.string(), Entry)]) + .refine( + (data) => { + if (typeof data === "boolean") return true + const serverIds = new Set(Object.values(LSPServer).map((server) => server.id)) + + return Object.entries(data).every(([id, config]) => { + if (config.disabled) return true + if (serverIds.has(id)) return true + return Boolean(config.extensions) + }) + }, + { + error: "For custom LSP servers, 'extensions' array is required.", + }, + ) + +export type Info = z.infer From f13778215ae8927e8bb500f421f835566eb9a017 Mon Sep 17 00:00:00 2001 From: Dax Date: Thu, 16 Apr 2026 21:35:47 -0400 Subject: [PATCH 044/335] perf: speed up skill directory discovery (#22990) --- packages/opencode/src/skill/index.ts | 60 +++++++++++++++++++--------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index b139b39e6e..dd5cc4e5d5 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -54,6 +54,16 @@ type State = { dirs: Set } +type DiscoveryState = { + matches: string[] + dirs: string[] +} + +type ScanState = { + matches: Set + dirs: Set +} + export interface Interface { readonly get: (name: string) => Effect.Effect readonly all: () => Effect.Effect @@ -102,8 +112,7 @@ const add = Effect.fnUntraced(function* (state: State, match: string, bus: Bus.I }) const scan = Effect.fnUntraced(function* ( - state: State, - bus: Bus.Interface, + state: ScanState, root: string, pattern: string, opts?: { dot?: boolean; scope?: string }, @@ -126,26 +135,26 @@ const scan = Effect.fnUntraced(function* ( }), ) - yield* Effect.forEach(matches, (match) => add(state, match, bus), { - concurrency: "unbounded", - discard: true, - }) + for (const match of matches) { + state.matches.add(match) + state.dirs.add(path.dirname(match)) + } }) -const loadSkills = Effect.fnUntraced(function* ( - state: State, +const discoverSkills = Effect.fnUntraced(function* ( config: Config.Interface, discovery: Discovery.Interface, - bus: Bus.Interface, fsys: AppFileSystem.Interface, directory: string, worktree: string, ) { + const state: ScanState = { matches: new Set(), dirs: new Set() } + if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) { for (const dir of EXTERNAL_DIRS) { const root = path.join(Global.Path.home, dir) if (!(yield* fsys.isDir(root))) continue - yield* scan(state, bus, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "global" }) + yield* scan(state, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "global" }) } const upDirs = yield* fsys @@ -153,13 +162,13 @@ const loadSkills = Effect.fnUntraced(function* ( .pipe(Effect.catch(() => Effect.succeed([] as string[]))) for (const root of upDirs) { - yield* scan(state, bus, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "project" }) + yield* scan(state, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "project" }) } } const configDirs = yield* config.directories() for (const dir of configDirs) { - yield* scan(state, bus, dir, OPENCODE_SKILL_PATTERN) + yield* scan(state, dir, OPENCODE_SKILL_PATTERN) } const cfg = yield* config.get() @@ -171,17 +180,28 @@ const loadSkills = Effect.fnUntraced(function* ( continue } - yield* scan(state, bus, dir, SKILL_PATTERN) + yield* scan(state, dir, SKILL_PATTERN) } for (const url of cfg.skills?.urls ?? []) { const pulledDirs = yield* discovery.pull(url) for (const dir of pulledDirs) { - state.dirs.add(dir) - yield* scan(state, bus, dir, SKILL_PATTERN) + yield* scan(state, dir, SKILL_PATTERN) } } + return { + matches: Array.from(state.matches), + dirs: Array.from(state.dirs), + } +}) + +const loadSkills = Effect.fnUntraced(function* (state: State, discovered: DiscoveryState, bus: Bus.Interface) { + yield* Effect.forEach(discovered.matches, (match) => add(state, match, bus), { + concurrency: "unbounded", + discard: true, + }) + log.info("init", { count: Object.keys(state.skills).length }) }) @@ -194,10 +214,15 @@ export const layer = Layer.effect( const config = yield* Config.Service const bus = yield* Bus.Service const fsys = yield* AppFileSystem.Service + const discovered = yield* InstanceState.make( + Effect.fn("Skill.discovery")(function* (ctx) { + return yield* discoverSkills(config, discovery, fsys, ctx.directory, ctx.worktree) + }), + ) const state = yield* InstanceState.make( Effect.fn("Skill.state")(function* (ctx) { const s: State = { skills: {}, dirs: new Set() } - yield* loadSkills(s, config, discovery, bus, fsys, ctx.directory, ctx.worktree) + yield* loadSkills(s, yield* InstanceState.get(discovered), bus) return s }), ) @@ -213,8 +238,7 @@ export const layer = Layer.effect( }) const dirs = Effect.fn("Skill.dirs")(function* () { - const s = yield* InstanceState.get(state) - return Array.from(s.dirs) + return (yield* InstanceState.get(discovered)).dirs }) const available = Effect.fn("Skill.available")(function* (agent?: Agent.Info) { From 5b9fa322551b77ddf2ab004c5491a82d37081b22 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 01:36:45 +0000 Subject: [PATCH 045/335] chore: generate --- packages/opencode/src/config/lsp.ts | 30 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/opencode/src/config/lsp.ts b/packages/opencode/src/config/lsp.ts index afb83908b9..233f7e523c 100644 --- a/packages/opencode/src/config/lsp.ts +++ b/packages/opencode/src/config/lsp.ts @@ -18,22 +18,20 @@ export const Entry = z.union([ }), ]) -export const Info = z - .union([z.literal(false), z.record(z.string(), Entry)]) - .refine( - (data) => { - if (typeof data === "boolean") return true - const serverIds = new Set(Object.values(LSPServer).map((server) => server.id)) +export const Info = z.union([z.literal(false), z.record(z.string(), Entry)]).refine( + (data) => { + if (typeof data === "boolean") return true + const serverIds = new Set(Object.values(LSPServer).map((server) => server.id)) - return Object.entries(data).every(([id, config]) => { - if (config.disabled) return true - if (serverIds.has(id)) return true - return Boolean(config.extensions) - }) - }, - { - error: "For custom LSP servers, 'extensions' array is required.", - }, - ) + return Object.entries(data).every(([id, config]) => { + if (config.disabled) return true + if (serverIds.has(id)) return true + return Boolean(config.extensions) + }) + }, + { + error: "For custom LSP servers, 'extensions' array is required.", + }, +) export type Info = z.infer From 9c87a144e879dd9b76c90cb1415e63005aac2843 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 21:43:57 -0400 Subject: [PATCH 046/335] refactor: normalize AccountRepo to canonical Effect service pattern (#22991) --- packages/opencode/src/account/account.ts | 4 +- packages/opencode/src/account/repo.ts | 264 +++++++++--------- packages/opencode/test/account/repo.test.ts | 78 +++--- .../opencode/test/account/service.test.ts | 20 +- .../opencode/test/share/share-next.test.ts | 2 +- 5 files changed, 184 insertions(+), 184 deletions(-) diff --git a/packages/opencode/src/account/account.ts b/packages/opencode/src/account/account.ts index 657c61b1e5..23981fd852 100644 --- a/packages/opencode/src/account/account.ts +++ b/packages/opencode/src/account/account.ts @@ -181,10 +181,10 @@ export interface Interface { export class Service extends Context.Service()("@opencode/Account") {} -export const layer: Layer.Layer = Layer.effect( +export const layer: Layer.Layer = Layer.effect( Service, Effect.gen(function* () { - const repo = yield* AccountRepo + const repo = yield* AccountRepo.Service const http = yield* HttpClient.HttpClient const httpRead = withTransientReadRetry(http) const httpOk = HttpClient.filterStatusOk(http) diff --git a/packages/opencode/src/account/repo.ts b/packages/opencode/src/account/repo.ts index 5d8a8e33f6..450db1bd74 100644 --- a/packages/opencode/src/account/repo.ts +++ b/packages/opencode/src/account/repo.ts @@ -13,154 +13,154 @@ type DbTransactionCallback = Parameters>[0] const ACCOUNT_STATE_ID = 1 -export namespace AccountRepo { - export interface Service { - readonly active: () => Effect.Effect, AccountRepoError> - readonly list: () => Effect.Effect - readonly remove: (accountID: AccountID) => Effect.Effect - readonly use: (accountID: AccountID, orgID: Option.Option) => Effect.Effect - readonly getRow: (accountID: AccountID) => Effect.Effect, AccountRepoError> - readonly persistToken: (input: { - accountID: AccountID - accessToken: AccessToken - refreshToken: RefreshToken - expiry: Option.Option - }) => Effect.Effect - readonly persistAccount: (input: { - id: AccountID - email: string - url: string - accessToken: AccessToken - refreshToken: RefreshToken - expiry: number - orgID: Option.Option - }) => Effect.Effect - } +export interface Interface { + readonly active: () => Effect.Effect, AccountRepoError> + readonly list: () => Effect.Effect + readonly remove: (accountID: AccountID) => Effect.Effect + readonly use: (accountID: AccountID, orgID: Option.Option) => Effect.Effect + readonly getRow: (accountID: AccountID) => Effect.Effect, AccountRepoError> + readonly persistToken: (input: { + accountID: AccountID + accessToken: AccessToken + refreshToken: RefreshToken + expiry: Option.Option + }) => Effect.Effect + readonly persistAccount: (input: { + id: AccountID + email: string + url: string + accessToken: AccessToken + refreshToken: RefreshToken + expiry: number + orgID: Option.Option + }) => Effect.Effect } -export class AccountRepo extends Context.Service()("@opencode/AccountRepo") { - static readonly layer: Layer.Layer = Layer.effect( - AccountRepo, - Effect.gen(function* () { - const decode = Schema.decodeUnknownSync(Info) +export class Service extends Context.Service()("@opencode/AccountRepo") {} - const query = (f: DbTransactionCallback) => - Effect.try({ - try: () => Database.use(f), - catch: (cause) => new AccountRepoError({ message: "Database operation failed", cause }), +export const layer: Layer.Layer = Layer.effect( + Service, + Effect.gen(function* () { + const decode = Schema.decodeUnknownSync(Info) + + const query = (f: DbTransactionCallback) => + Effect.try({ + try: () => Database.use(f), + catch: (cause) => new AccountRepoError({ message: "Database operation failed", cause }), + }) + + const tx = (f: DbTransactionCallback) => + Effect.try({ + try: () => Database.transaction(f), + catch: (cause) => new AccountRepoError({ message: "Database operation failed", cause }), + }) + + const current = (db: DbClient) => { + const state = db.select().from(AccountStateTable).where(eq(AccountStateTable.id, ACCOUNT_STATE_ID)).get() + if (!state?.active_account_id) return + const account = db.select().from(AccountTable).where(eq(AccountTable.id, state.active_account_id)).get() + if (!account) return + return { ...account, active_org_id: state.active_org_id ?? null } + } + + const state = (db: DbClient, accountID: AccountID, orgID: Option.Option) => { + const id = Option.getOrNull(orgID) + return db + .insert(AccountStateTable) + .values({ id: ACCOUNT_STATE_ID, active_account_id: accountID, active_org_id: id }) + .onConflictDoUpdate({ + target: AccountStateTable.id, + set: { active_account_id: accountID, active_org_id: id }, }) + .run() + } - const tx = (f: DbTransactionCallback) => - Effect.try({ - try: () => Database.transaction(f), - catch: (cause) => new AccountRepoError({ message: "Database operation failed", cause }), - }) + const active = Effect.fn("AccountRepo.active")(() => + query((db) => current(db)).pipe(Effect.map((row) => (row ? Option.some(decode(row)) : Option.none()))), + ) - const current = (db: DbClient) => { - const state = db.select().from(AccountStateTable).where(eq(AccountStateTable.id, ACCOUNT_STATE_ID)).get() - if (!state?.active_account_id) return - const account = db.select().from(AccountTable).where(eq(AccountTable.id, state.active_account_id)).get() - if (!account) return - return { ...account, active_org_id: state.active_org_id ?? null } - } + const list = Effect.fn("AccountRepo.list")(() => + query((db) => + db + .select() + .from(AccountTable) + .all() + .map((row: AccountRow) => decode({ ...row, active_org_id: null })), + ), + ) - const state = (db: DbClient, accountID: AccountID, orgID: Option.Option) => { - const id = Option.getOrNull(orgID) - return db - .insert(AccountStateTable) - .values({ id: ACCOUNT_STATE_ID, active_account_id: accountID, active_org_id: id }) - .onConflictDoUpdate({ - target: AccountStateTable.id, - set: { active_account_id: accountID, active_org_id: id }, - }) + const remove = Effect.fn("AccountRepo.remove")((accountID: AccountID) => + tx((db) => { + db.update(AccountStateTable) + .set({ active_account_id: null, active_org_id: null }) + .where(eq(AccountStateTable.active_account_id, accountID)) .run() - } + db.delete(AccountTable).where(eq(AccountTable.id, accountID)).run() + }).pipe(Effect.asVoid), + ) - const active = Effect.fn("AccountRepo.active")(() => - query((db) => current(db)).pipe(Effect.map((row) => (row ? Option.some(decode(row)) : Option.none()))), - ) + const use = Effect.fn("AccountRepo.use")((accountID: AccountID, orgID: Option.Option) => + query((db) => state(db, accountID, orgID)).pipe(Effect.asVoid), + ) - const list = Effect.fn("AccountRepo.list")(() => - query((db) => - db - .select() - .from(AccountTable) - .all() - .map((row: AccountRow) => decode({ ...row, active_org_id: null })), - ), - ) + const getRow = Effect.fn("AccountRepo.getRow")((accountID: AccountID) => + query((db) => db.select().from(AccountTable).where(eq(AccountTable.id, accountID)).get()).pipe( + Effect.map(Option.fromNullishOr), + ), + ) - const remove = Effect.fn("AccountRepo.remove")((accountID: AccountID) => - tx((db) => { - db.update(AccountStateTable) - .set({ active_account_id: null, active_org_id: null }) - .where(eq(AccountStateTable.active_account_id, accountID)) - .run() - db.delete(AccountTable).where(eq(AccountTable.id, accountID)).run() - }).pipe(Effect.asVoid), - ) + const persistToken = Effect.fn("AccountRepo.persistToken")((input) => + query((db) => + db + .update(AccountTable) + .set({ + access_token: input.accessToken, + refresh_token: input.refreshToken, + token_expiry: Option.getOrNull(input.expiry), + }) + .where(eq(AccountTable.id, input.accountID)) + .run(), + ).pipe(Effect.asVoid), + ) - const use = Effect.fn("AccountRepo.use")((accountID: AccountID, orgID: Option.Option) => - query((db) => state(db, accountID, orgID)).pipe(Effect.asVoid), - ) + const persistAccount = Effect.fn("AccountRepo.persistAccount")((input) => + tx((db) => { + const url = normalizeServerUrl(input.url) - const getRow = Effect.fn("AccountRepo.getRow")((accountID: AccountID) => - query((db) => db.select().from(AccountTable).where(eq(AccountTable.id, accountID)).get()).pipe( - Effect.map(Option.fromNullishOr), - ), - ) - - const persistToken = Effect.fn("AccountRepo.persistToken")((input) => - query((db) => - db - .update(AccountTable) - .set({ - access_token: input.accessToken, - refresh_token: input.refreshToken, - token_expiry: Option.getOrNull(input.expiry), - }) - .where(eq(AccountTable.id, input.accountID)) - .run(), - ).pipe(Effect.asVoid), - ) - - const persistAccount = Effect.fn("AccountRepo.persistAccount")((input) => - tx((db) => { - const url = normalizeServerUrl(input.url) - - db.insert(AccountTable) - .values({ - id: input.id, + db.insert(AccountTable) + .values({ + id: input.id, + email: input.email, + url, + access_token: input.accessToken, + refresh_token: input.refreshToken, + token_expiry: input.expiry, + }) + .onConflictDoUpdate({ + target: AccountTable.id, + set: { email: input.email, url, access_token: input.accessToken, refresh_token: input.refreshToken, token_expiry: input.expiry, - }) - .onConflictDoUpdate({ - target: AccountTable.id, - set: { - email: input.email, - url, - access_token: input.accessToken, - refresh_token: input.refreshToken, - token_expiry: input.expiry, - }, - }) - .run() - void state(db, input.id, input.orgID) - }).pipe(Effect.asVoid), - ) + }, + }) + .run() + void state(db, input.id, input.orgID) + }).pipe(Effect.asVoid), + ) - return AccountRepo.of({ - active, - list, - remove, - use, - getRow, - persistToken, - persistAccount, - }) - }), - ) -} + return Service.of({ + active, + list, + remove, + use, + getRow, + persistToken, + persistAccount, + }) + }), +) + +export * as AccountRepo from "./repo" diff --git a/packages/opencode/test/account/repo.test.ts b/packages/opencode/test/account/repo.test.ts index 93d0481521..8e59b85b31 100644 --- a/packages/opencode/test/account/repo.test.ts +++ b/packages/opencode/test/account/repo.test.ts @@ -18,14 +18,14 @@ const it = testEffect(Layer.merge(AccountRepo.layer, truncate)) it.live("list returns empty when no accounts exist", () => Effect.gen(function* () { - const accounts = yield* AccountRepo.use((r) => r.list()) + const accounts = yield* AccountRepo.Service.use((r) => r.list()) expect(accounts).toEqual([]) }), ) it.live("active returns none when no accounts exist", () => Effect.gen(function* () { - const active = yield* AccountRepo.use((r) => r.active()) + const active = yield* AccountRepo.Service.use((r) => r.active()) expect(Option.isNone(active)).toBe(true) }), ) @@ -33,7 +33,7 @@ it.live("active returns none when no accounts exist", () => it.live("persistAccount inserts and getRow retrieves", () => Effect.gen(function* () { const id = AccountID.make("user-1") - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id, email: "test@example.com", @@ -45,13 +45,13 @@ it.live("persistAccount inserts and getRow retrieves", () => }), ) - const row = yield* AccountRepo.use((r) => r.getRow(id)) + const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) expect(Option.isSome(row)).toBe(true) const value = Option.getOrThrow(row) expect(value.id).toBe(AccountID.make("user-1")) expect(value.email).toBe("test@example.com") - const active = yield* AccountRepo.use((r) => r.active()) + const active = yield* AccountRepo.Service.use((r) => r.active()) expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-1")) }), ) @@ -60,7 +60,7 @@ it.live("persistAccount normalizes trailing slashes in stored server URLs", () = Effect.gen(function* () { const id = AccountID.make("user-1") - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id, email: "test@example.com", @@ -72,9 +72,9 @@ it.live("persistAccount normalizes trailing slashes in stored server URLs", () = }), ) - const row = yield* AccountRepo.use((r) => r.getRow(id)) - const active = yield* AccountRepo.use((r) => r.active()) - const list = yield* AccountRepo.use((r) => r.list()) + const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) + const active = yield* AccountRepo.Service.use((r) => r.active()) + const list = yield* AccountRepo.Service.use((r) => r.list()) expect(Option.getOrThrow(row).url).toBe("https://control.example.com") expect(Option.getOrThrow(active).url).toBe("https://control.example.com") @@ -87,7 +87,7 @@ it.live("persistAccount sets the active account and org", () => const id1 = AccountID.make("user-1") const id2 = AccountID.make("user-2") - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id: id1, email: "first@example.com", @@ -99,7 +99,7 @@ it.live("persistAccount sets the active account and org", () => }), ) - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id: id2, email: "second@example.com", @@ -112,7 +112,7 @@ it.live("persistAccount sets the active account and org", () => ) // Last persisted account is active with its org - const active = yield* AccountRepo.use((r) => r.active()) + const active = yield* AccountRepo.Service.use((r) => r.active()) expect(Option.isSome(active)).toBe(true) expect(Option.getOrThrow(active).id).toBe(AccountID.make("user-2")) expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-2")) @@ -124,7 +124,7 @@ it.live("list returns all accounts", () => const id1 = AccountID.make("user-1") const id2 = AccountID.make("user-2") - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id: id1, email: "a@example.com", @@ -136,7 +136,7 @@ it.live("list returns all accounts", () => }), ) - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id: id2, email: "b@example.com", @@ -148,7 +148,7 @@ it.live("list returns all accounts", () => }), ) - const accounts = yield* AccountRepo.use((r) => r.list()) + const accounts = yield* AccountRepo.Service.use((r) => r.list()) expect(accounts.length).toBe(2) expect(accounts.map((a) => a.email).sort()).toEqual(["a@example.com", "b@example.com"]) }), @@ -158,7 +158,7 @@ it.live("remove deletes an account", () => Effect.gen(function* () { const id = AccountID.make("user-1") - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id, email: "test@example.com", @@ -170,9 +170,9 @@ it.live("remove deletes an account", () => }), ) - yield* AccountRepo.use((r) => r.remove(id)) + yield* AccountRepo.Service.use((r) => r.remove(id)) - const row = yield* AccountRepo.use((r) => r.getRow(id)) + const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) expect(Option.isNone(row)).toBe(true) }), ) @@ -182,7 +182,7 @@ it.live("use stores the selected org and marks the account active", () => const id1 = AccountID.make("user-1") const id2 = AccountID.make("user-2") - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id: id1, email: "first@example.com", @@ -194,7 +194,7 @@ it.live("use stores the selected org and marks the account active", () => }), ) - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id: id2, email: "second@example.com", @@ -206,13 +206,13 @@ it.live("use stores the selected org and marks the account active", () => }), ) - yield* AccountRepo.use((r) => r.use(id1, Option.some(OrgID.make("org-99")))) - const active1 = yield* AccountRepo.use((r) => r.active()) + yield* AccountRepo.Service.use((r) => r.use(id1, Option.some(OrgID.make("org-99")))) + const active1 = yield* AccountRepo.Service.use((r) => r.active()) expect(Option.getOrThrow(active1).id).toBe(id1) expect(Option.getOrThrow(active1).active_org_id).toBe(OrgID.make("org-99")) - yield* AccountRepo.use((r) => r.use(id1, Option.none())) - const active2 = yield* AccountRepo.use((r) => r.active()) + yield* AccountRepo.Service.use((r) => r.use(id1, Option.none())) + const active2 = yield* AccountRepo.Service.use((r) => r.active()) expect(Option.getOrThrow(active2).active_org_id).toBeNull() }), ) @@ -221,7 +221,7 @@ it.live("persistToken updates token fields", () => Effect.gen(function* () { const id = AccountID.make("user-1") - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id, email: "test@example.com", @@ -234,7 +234,7 @@ it.live("persistToken updates token fields", () => ) const expiry = Date.now() + 7200_000 - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistToken({ accountID: id, accessToken: AccessToken.make("new_token"), @@ -243,7 +243,7 @@ it.live("persistToken updates token fields", () => }), ) - const row = yield* AccountRepo.use((r) => r.getRow(id)) + const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) const value = Option.getOrThrow(row) expect(value.access_token).toBe(AccessToken.make("new_token")) expect(value.refresh_token).toBe(RefreshToken.make("new_refresh")) @@ -255,7 +255,7 @@ it.live("persistToken with no expiry sets token_expiry to null", () => Effect.gen(function* () { const id = AccountID.make("user-1") - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id, email: "test@example.com", @@ -267,7 +267,7 @@ it.live("persistToken with no expiry sets token_expiry to null", () => }), ) - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistToken({ accountID: id, accessToken: AccessToken.make("new_token"), @@ -276,7 +276,7 @@ it.live("persistToken with no expiry sets token_expiry to null", () => }), ) - const row = yield* AccountRepo.use((r) => r.getRow(id)) + const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) expect(Option.getOrThrow(row).token_expiry).toBeNull() }), ) @@ -285,7 +285,7 @@ it.live("persistAccount upserts on conflict", () => Effect.gen(function* () { const id = AccountID.make("user-1") - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id, email: "test@example.com", @@ -297,7 +297,7 @@ it.live("persistAccount upserts on conflict", () => }), ) - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id, email: "test@example.com", @@ -309,14 +309,14 @@ it.live("persistAccount upserts on conflict", () => }), ) - const accounts = yield* AccountRepo.use((r) => r.list()) + const accounts = yield* AccountRepo.Service.use((r) => r.list()) expect(accounts.length).toBe(1) - const row = yield* AccountRepo.use((r) => r.getRow(id)) + const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) const value = Option.getOrThrow(row) expect(value.access_token).toBe(AccessToken.make("at_v2")) - const active = yield* AccountRepo.use((r) => r.active()) + const active = yield* AccountRepo.Service.use((r) => r.active()) expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-2")) }), ) @@ -325,7 +325,7 @@ it.live("remove clears active state when deleting the active account", () => Effect.gen(function* () { const id = AccountID.make("user-1") - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id, email: "test@example.com", @@ -337,16 +337,16 @@ it.live("remove clears active state when deleting the active account", () => }), ) - yield* AccountRepo.use((r) => r.remove(id)) + yield* AccountRepo.Service.use((r) => r.remove(id)) - const active = yield* AccountRepo.use((r) => r.active()) + const active = yield* AccountRepo.Service.use((r) => r.active()) expect(Option.isNone(active)).toBe(true) }), ) it.live("getRow returns none for nonexistent account", () => Effect.gen(function* () { - const row = yield* AccountRepo.use((r) => r.getRow(AccountID.make("nope"))) + const row = yield* AccountRepo.Service.use((r) => r.getRow(AccountID.make("nope"))) expect(Option.isNone(row)).toBe(true) }), ) diff --git a/packages/opencode/test/account/service.test.ts b/packages/opencode/test/account/service.test.ts index 053fd2a0ed..f0daab3a15 100644 --- a/packages/opencode/test/account/service.test.ts +++ b/packages/opencode/test/account/service.test.ts @@ -122,7 +122,7 @@ it.live("login maps transport failures to account transport errors", () => it.live("orgsByAccount groups orgs per account", () => Effect.gen(function* () { - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id: AccountID.make("user-1"), email: "one@example.com", @@ -134,7 +134,7 @@ it.live("orgsByAccount groups orgs per account", () => }), ) - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id: AccountID.make("user-2"), email: "two@example.com", @@ -177,7 +177,7 @@ it.live("token refresh persists the new token", () => Effect.gen(function* () { const id = AccountID.make("user-1") - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id, email: "user@example.com", @@ -206,7 +206,7 @@ it.live("token refresh persists the new token", () => expect(Option.getOrThrow(token)).toBeDefined() expect(String(Option.getOrThrow(token))).toBe("at_new") - const row = yield* AccountRepo.use((r) => r.getRow(id)) + const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) const value = Option.getOrThrow(row) expect(value.access_token).toBe(AccessToken.make("at_new")) expect(value.refresh_token).toBe(RefreshToken.make("rt_new")) @@ -218,7 +218,7 @@ it.live("token refreshes before expiry when inside the eager refresh window", () Effect.gen(function* () { const id = AccountID.make("user-1") - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id, email: "user@example.com", @@ -251,7 +251,7 @@ it.live("token refreshes before expiry when inside the eager refresh window", () expect(String(Option.getOrThrow(token))).toBe("at_new") expect(refreshCalls).toBe(1) - const row = yield* AccountRepo.use((r) => r.getRow(id)) + const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) const value = Option.getOrThrow(row) expect(value.access_token).toBe(AccessToken.make("at_new")) expect(value.refresh_token).toBe(RefreshToken.make("rt_new")) @@ -262,7 +262,7 @@ it.live("concurrent config and token requests coalesce token refresh", () => Effect.gen(function* () { const id = AccountID.make("user-1") - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id, email: "user@example.com", @@ -315,7 +315,7 @@ it.live("concurrent config and token requests coalesce token refresh", () => expect(String(Option.getOrThrow(token))).toBe("at_new") expect(refreshCalls).toBe(1) - const row = yield* AccountRepo.use((r) => r.getRow(id)) + const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) const value = Option.getOrThrow(row) expect(value.access_token).toBe(AccessToken.make("at_new")) expect(value.refresh_token).toBe(RefreshToken.make("rt_new")) @@ -326,7 +326,7 @@ it.live("config sends the selected org header", () => Effect.gen(function* () { const id = AccountID.make("user-1") - yield* AccountRepo.use((r) => + yield* AccountRepo.Service.use((r) => r.persistAccount({ id, email: "user@example.com", @@ -388,7 +388,7 @@ it.live("poll stores the account and first org on success", () => expect(res.email).toBe("user@example.com") } - const active = yield* AccountRepo.use((r) => r.active()) + const active = yield* AccountRepo.Service.use((r) => r.active()) expect(Option.getOrThrow(active)).toEqual( expect.objectContaining({ id: "user-1", diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index 2359f06a31..930c4062f6 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/packages/opencode/test/share/share-next.test.ts @@ -72,7 +72,7 @@ const share = (id: SessionID) => Database.use((db) => db.select().from(SessionShareTable).where(eq(SessionShareTable.session_id, id)).get()) const seed = (url: string, org?: string) => - AccountRepo.use((repo) => + AccountRepo.Service.use((repo) => repo.persistAccount({ id: AccountID.make("account-1"), email: "user@example.com", From 4f8986aa48cbab66ca6e72272c3c7d27ffc8e0eb Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 21:51:02 -0400 Subject: [PATCH 047/335] refactor: unwrap Question namespace + fix script to emit "." for index.ts (#22992) --- .../script/unwrap-and-self-reexport.ts | 11 +- packages/opencode/src/question/index.ts | 384 +++++++++--------- 2 files changed, 200 insertions(+), 195 deletions(-) diff --git a/packages/opencode/script/unwrap-and-self-reexport.ts b/packages/opencode/script/unwrap-and-self-reexport.ts index 5ae703182e..09256f3a51 100644 --- a/packages/opencode/script/unwrap-and-self-reexport.ts +++ b/packages/opencode/script/unwrap-and-self-reexport.ts @@ -207,10 +207,15 @@ const rewrittenBody = dedented.map(rewriteLine) // Assemble the new file. Collapse multiple trailing blank lines so the // self-reexport sits cleanly at the end. +// +// When the file is itself `index.ts`, prefer `"."` over `"./index"` — both are +// valid but `"."` matches the existing convention in the codebase (e.g. +// pty/index.ts, file/index.ts, etc.) and avoids referencing "index" literally. const basename = path.basename(absPath, ".ts") +const reexportSource = basename === "index" ? "." : `./${basename}` const assembled = [...before, ...rewrittenBody, ...after].join("\n") const trimmed = assembled.replace(/\s+$/g, "") -const output = `${trimmed}\n\nexport * as ${nsName} from "./${basename}"\n` +const output = `${trimmed}\n\nexport * as ${nsName} from "${reexportSource}"\n` if (dryRun) { console.log(`--- dry run: ${path.relative(process.cwd(), absPath)} ---`) @@ -218,7 +223,7 @@ if (dryRun) { console.log(`body lines: ${body.length}`) console.log(`declared names: ${Array.from(declaredNames).join(", ") || "(none)"}`) console.log(`self-refs rewr: ${rewriteCount}`) - console.log(`self-reexport: export * as ${nsName} from "./${basename}"`) + console.log(`self-reexport: export * as ${nsName} from "${reexportSource}"`) console.log(`output preview (last 10 lines):`) const outputLines = output.split("\n") for (const l of outputLines.slice(Math.max(0, outputLines.length - 10))) { @@ -231,7 +236,7 @@ fs.writeFileSync(absPath, output) console.log(`unwrapped ${path.relative(process.cwd(), absPath)} → ${nsName}`) console.log(` body lines: ${body.length}`) console.log(` self-refs rewr: ${rewriteCount}`) -console.log(` self-reexport: export * as ${nsName} from "./${basename}"`) +console.log(` self-reexport: export * as ${nsName} from "${reexportSource}"`) console.log("") console.log("Next: verify with") console.log(" bunx --bun tsgo --noEmit") diff --git a/packages/opencode/src/question/index.ts b/packages/opencode/src/question/index.ts index 627d04564d..3b377c9827 100644 --- a/packages/opencode/src/question/index.ts +++ b/packages/opencode/src/question/index.ts @@ -8,222 +8,222 @@ import { Log } from "@/util" import { withStatics } from "@/util/schema" import { QuestionID } from "./schema" -export namespace Question { - const log = Log.create({ service: "question" }) +const log = Log.create({ service: "question" }) - // Schemas +// Schemas - export class Option extends Schema.Class` if that directory exists. - * 5. Commit, push with `--no-verify`, and open a PR titled after the - * namespace. - * - * Usage: - * - * bun script/batch-unwrap-pr.ts src/file/ignore.ts - * bun script/batch-unwrap-pr.ts src/file/ignore.ts src/file/watcher.ts # multiple - * bun script/batch-unwrap-pr.ts --dry-run src/file/ignore.ts # plan only - * - * Repo assumptions: - * - * - Main checkout at /Users/kit/code/open-source/opencode (configurable via - * --repo-root=...). - * - Worktree root at /Users/kit/code/open-source/opencode-worktrees - * (configurable via --worktree-root=...). - * - * The script does NOT enable auto-merge; that's a separate manual step if we - * want it. - */ - -import fs from "node:fs" -import path from "node:path" -import { spawnSync, type SpawnSyncReturns } from "node:child_process" - -type Cmd = string[] - -function run( - cwd: string, - cmd: Cmd, - opts: { capture?: boolean; allowFail?: boolean; stdin?: string } = {}, -): SpawnSyncReturns { - const result = spawnSync(cmd[0], cmd.slice(1), { - cwd, - stdio: opts.capture ? ["pipe", "pipe", "pipe"] : ["inherit", "inherit", "inherit"], - encoding: "utf-8", - input: opts.stdin, - }) - if (!opts.allowFail && result.status !== 0) { - const label = `${path.basename(cmd[0])} ${cmd.slice(1).join(" ")}` - console.error(`[fail] ${label} (cwd=${cwd})`) - if (opts.capture) { - if (result.stdout) console.error(result.stdout) - if (result.stderr) console.error(result.stderr) - } - process.exit(result.status ?? 1) - } - return result -} - -function fileSlug(fileArg: string): string { - // src/file/ignore.ts → file-ignore - return fileArg - .replace(/^src\//, "") - .replace(/\.tsx?$/, "") - .replace(/[\/_]/g, "-") -} - -function readNamespace(absFile: string): string { - const content = fs.readFileSync(absFile, "utf-8") - const match = content.match(/^export\s+namespace\s+(\w+)\s*\{/m) - if (!match) { - console.error(`no \`export namespace\` found in ${absFile}`) - process.exit(1) - } - return match[1] -} - -// --------------------------------------------------------------------------- - -const args = process.argv.slice(2) -const dryRun = args.includes("--dry-run") -const repoRoot = ( - args.find((a) => a.startsWith("--repo-root=")) ?? "--repo-root=/Users/kit/code/open-source/opencode" -).split("=")[1] -const worktreeRoot = ( - args.find((a) => a.startsWith("--worktree-root=")) ?? "--worktree-root=/Users/kit/code/open-source/opencode-worktrees" -).split("=")[1] -const targets = args.filter((a) => !a.startsWith("--")) - -if (targets.length === 0) { - console.error("Usage: bun script/batch-unwrap-pr.ts [more files...] [--dry-run]") - process.exit(1) -} - -if (!fs.existsSync(worktreeRoot)) fs.mkdirSync(worktreeRoot, { recursive: true }) - -for (const rel of targets) { - const absSrc = path.join(repoRoot, "packages", "opencode", rel) - if (!fs.existsSync(absSrc)) { - console.error(`skip ${rel}: file does not exist under ${repoRoot}/packages/opencode`) - continue - } - const slug = fileSlug(rel) - const branch = `kit/ns-${slug}` - const wt = path.join(worktreeRoot, `ns-${slug}`) - const ns = readNamespace(absSrc) - - console.log(`\n=== ${rel} → ${ns} (branch=${branch} wt=${path.basename(wt)}) ===`) - - if (dryRun) { - console.log(` would create worktree ${wt}`) - console.log(` would run unwrap on packages/opencode/${rel}`) - console.log(` would commit, push, and open PR`) - continue - } - - // Sync dev (fetch only; we branch off origin/dev directly). - run(repoRoot, ["git", "fetch", "origin", "dev", "--quiet"]) - - // Create worktree + branch. - if (fs.existsSync(wt)) { - console.log(` worktree already exists at ${wt}; skipping`) - continue - } - run(repoRoot, ["git", "worktree", "add", "-b", branch, wt, "origin/dev"]) - - // Symlink node_modules so bun/tsgo work without a full install. - // We link both the repo root and packages/opencode, since the opencode - // package has its own local node_modules (including bunfig.toml preload deps - // like @opentui/solid) that aren't hoisted to the root. - const wtRootNodeModules = path.join(wt, "node_modules") - if (!fs.existsSync(wtRootNodeModules)) { - fs.symlinkSync(path.join(repoRoot, "node_modules"), wtRootNodeModules) - } - const wtOpencode = path.join(wt, "packages", "opencode") - const wtOpencodeNodeModules = path.join(wtOpencode, "node_modules") - if (!fs.existsSync(wtOpencodeNodeModules)) { - fs.symlinkSync(path.join(repoRoot, "packages", "opencode", "node_modules"), wtOpencodeNodeModules) - } - const wtTarget = path.join(wt, "packages", "opencode", rel) - - // Baseline tsgo output (pre-change). - const baselinePath = path.join(wt, ".ns-baseline.txt") - const baseline = run(wtOpencode, ["bunx", "--bun", "tsgo", "--noEmit"], { capture: true, allowFail: true }) - fs.writeFileSync(baselinePath, (baseline.stdout ?? "") + (baseline.stderr ?? "")) - - // Run the unwrap script from the MAIN repo checkout (where the tooling - // lives) targeting the worktree's file by absolute path. We run from the - // worktree root (not `packages/opencode`) to avoid triggering the - // bunfig.toml preload, which needs `@opentui/solid` that only the TUI - // workspace has installed. - const unwrapScript = path.join(repoRoot, "packages", "opencode", "script", "unwrap-and-self-reexport.ts") - run(wt, ["bun", unwrapScript, wtTarget]) - - // Post-change tsgo. - const after = run(wtOpencode, ["bunx", "--bun", "tsgo", "--noEmit"], { capture: true, allowFail: true }) - const afterText = (after.stdout ?? "") + (after.stderr ?? "") - - // Compare line-sets to detect NEW tsgo errors. - const sanitize = (s: string) => - s - .split("\n") - .map((l) => l.replace(/\s+$/, "")) - .filter(Boolean) - .sort() - .join("\n") - const baselineSorted = sanitize(fs.readFileSync(baselinePath, "utf-8")) - const afterSorted = sanitize(afterText) - if (baselineSorted !== afterSorted) { - console.log(` tsgo output differs from baseline. Showing diff:`) - const diffResult = spawnSync("diff", ["-u", baselinePath, "-"], { input: afterText, encoding: "utf-8" }) - if (diffResult.stdout) console.log(diffResult.stdout) - if (diffResult.stderr) console.log(diffResult.stderr) - console.error(` aborting ${rel}; investigate manually in ${wt}`) - process.exit(1) - } - - // SDK build. - run(wtOpencode, ["bun", "run", "--conditions=browser", "./src/index.ts", "generate"], { capture: true }) - - // Run tests for the directory, if a matching test dir exists. - const dirName = path.basename(path.dirname(rel)) - const testDir = path.join(wt, "packages", "opencode", "test", dirName) - if (fs.existsSync(testDir)) { - const testResult = run(wtOpencode, ["bun", "run", "test", `test/${dirName}`], { capture: true, allowFail: true }) - const combined = (testResult.stdout ?? "") + (testResult.stderr ?? "") - if (testResult.status !== 0) { - console.error(combined) - console.error(` tests failed for ${rel}; aborting`) - process.exit(1) - } - // Surface the summary line if present. - const summary = combined - .split("\n") - .filter((l) => /\bpass\b|\bfail\b/.test(l)) - .slice(-3) - .join("\n") - if (summary) console.log(` tests: ${summary.replace(/\n/g, " | ")}`) - } else { - console.log(` tests: no test/${dirName} directory, skipping`) - } - - // Clean up baseline file before committing. - fs.unlinkSync(baselinePath) - - // Commit, push, open PR. - const commitMsg = `refactor: unwrap ${ns} namespace + self-reexport` - run(wt, ["git", "add", "-A"]) - run(wt, ["git", "commit", "-m", commitMsg]) - run(wt, ["git", "push", "-u", "origin", branch, "--no-verify"]) - - const prBody = [ - "## Summary", - `- Unwrap the \`${ns}\` namespace in \`packages/opencode/${rel}\` to flat top-level exports.`, - `- Append \`export * as ${ns} from "./${path.basename(rel, ".ts")}"\` so consumers keep the same \`${ns}.x\` import ergonomics.`, - "", - "## Verification (local)", - "- `bunx --bun tsgo --noEmit` — no new errors vs baseline.", - "- `bun run --conditions=browser ./src/index.ts generate` — clean.", - `- \`bun run test test/${dirName}\` — all pass (if applicable).`, - ].join("\n") - run(wt, ["gh", "pr", "create", "--title", commitMsg, "--base", "dev", "--body", prBody]) - - console.log(` PR opened for ${rel}`) -} diff --git a/packages/opencode/script/collapse-barrel.ts b/packages/opencode/script/collapse-barrel.ts deleted file mode 100644 index 05bb11589c..0000000000 --- a/packages/opencode/script/collapse-barrel.ts +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env bun -/** - * Collapse a single-namespace barrel directory into a dir/index.ts module. - * - * Given a directory `src/foo/` that contains: - * - * - `index.ts` (exactly `export * as Foo from "./foo"`) - * - `foo.ts` (the real implementation) - * - zero or more sibling files - * - * this script: - * - * 1. Deletes the old `index.ts` barrel. - * 2. `git mv`s `foo.ts` → `index.ts` so the implementation IS the directory entry. - * 3. Appends `export * as Foo from "."` to the new `index.ts`. - * 4. Rewrites any same-directory sibling `*.ts` files that imported - * `./foo` (with or without the namespace name) to import `"."` instead. - * - * Consumer files outside the directory keep importing from the directory - * (`"@/foo"` / `"../foo"` / etc.) and continue to work, because - * `dir/index.ts` now provides the `Foo` named export directly. - * - * Usage: - * - * bun script/collapse-barrel.ts src/bus - * bun script/collapse-barrel.ts src/bus --dry-run - * - * Notes: - * - * - Only works on directories whose barrel is a single - * `export * as Name from "./file"` line. Refuses otherwise. - * - Refuses if the implementation file name already conflicts with - * `index.ts`. - * - Safe to run repeatedly: a second run on an already-collapsed dir - * will exit with a clear message. - */ - -import fs from "node:fs" -import path from "node:path" -import { spawnSync } from "node:child_process" - -const args = process.argv.slice(2) -const dryRun = args.includes("--dry-run") -const targetArg = args.find((a) => !a.startsWith("--")) - -if (!targetArg) { - console.error("Usage: bun script/collapse-barrel.ts [--dry-run]") - process.exit(1) -} - -const dir = path.resolve(targetArg) -const indexPath = path.join(dir, "index.ts") - -if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) { - console.error(`Not a directory: ${dir}`) - process.exit(1) -} -if (!fs.existsSync(indexPath)) { - console.error(`No index.ts in ${dir}`) - process.exit(1) -} - -// Validate barrel shape. -const indexContent = fs.readFileSync(indexPath, "utf-8").trim() -const match = indexContent.match(/^export\s+\*\s+as\s+(\w+)\s+from\s+["']\.\/([^"']+)["']\s*;?\s*$/) -if (!match) { - console.error(`Not a simple single-namespace barrel:\n${indexContent}`) - process.exit(1) -} -const namespaceName = match[1] -const implRel = match[2].replace(/\.ts$/, "") -const implPath = path.join(dir, `${implRel}.ts`) - -if (!fs.existsSync(implPath)) { - console.error(`Implementation file not found: ${implPath}`) - process.exit(1) -} - -if (implRel === "index") { - console.error(`Nothing to do — impl file is already index.ts`) - process.exit(0) -} - -console.log(`Collapsing ${path.relative(process.cwd(), dir)}`) -console.log(` namespace: ${namespaceName}`) -console.log(` impl file: ${implRel}.ts → index.ts`) - -// Figure out which sibling files need rewriting. -const siblings = fs - .readdirSync(dir) - .filter((f) => f.endsWith(".ts") || f.endsWith(".tsx")) - .filter((f) => f !== "index.ts" && f !== `${implRel}.ts`) - .map((f) => path.join(dir, f)) - -type SiblingEdit = { file: string; content: string } -const siblingEdits: SiblingEdit[] = [] - -for (const sibling of siblings) { - const content = fs.readFileSync(sibling, "utf-8") - // Match any import or re-export referring to "./" inside this directory. - const siblingRegex = new RegExp(`(from\\s*["'])\\.\\/${implRel.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&")}(["'])`, "g") - if (!siblingRegex.test(content)) continue - const updated = content.replace(siblingRegex, `$1.$2`) - siblingEdits.push({ file: sibling, content: updated }) -} - -if (siblingEdits.length > 0) { - console.log(` sibling rewrites: ${siblingEdits.length}`) - for (const edit of siblingEdits) { - console.log(` ${path.relative(process.cwd(), edit.file)}`) - } -} else { - console.log(` sibling rewrites: none`) -} - -if (dryRun) { - console.log(`\n(dry run) would:`) - console.log(` - delete ${path.relative(process.cwd(), indexPath)}`) - console.log(` - git mv ${path.relative(process.cwd(), implPath)} ${path.relative(process.cwd(), indexPath)}`) - console.log(` - append \`export * as ${namespaceName} from "."\` to the new index.ts`) - for (const edit of siblingEdits) { - console.log(` - rewrite sibling: ${path.relative(process.cwd(), edit.file)}`) - } - process.exit(0) -} - -// Apply: remove the old barrel, git-mv the impl onto it, then rewrite content. -// We can't git-mv on top of an existing tracked file, so we remove the barrel first. -function runGit(...cmd: string[]) { - const res = spawnSync("git", cmd, { stdio: "inherit" }) - if (res.status !== 0) { - console.error(`git ${cmd.join(" ")} failed`) - process.exit(res.status ?? 1) - } -} - -// Step 1: remove the barrel -runGit("rm", "-f", indexPath) - -// Step 2: rename the impl file into index.ts -runGit("mv", implPath, indexPath) - -// Step 3: append the self-reexport to the new index.ts -const newContent = fs.readFileSync(indexPath, "utf-8") -const trimmed = newContent.endsWith("\n") ? newContent : newContent + "\n" -fs.writeFileSync(indexPath, `${trimmed}\nexport * as ${namespaceName} from "."\n`) -console.log(` appended: export * as ${namespaceName} from "."`) - -// Step 4: rewrite siblings -for (const edit of siblingEdits) { - fs.writeFileSync(edit.file, edit.content) -} -if (siblingEdits.length > 0) { - console.log(` rewrote ${siblingEdits.length} sibling file(s)`) -} - -console.log(`\nDone. Verify with:`) -console.log(` cd packages/opencode`) -console.log(` bunx --bun tsgo --noEmit`) -console.log(` bun run --conditions=browser ./src/index.ts generate`) -console.log(` bun run test`) diff --git a/packages/opencode/script/unwrap-and-self-reexport.ts b/packages/opencode/script/unwrap-and-self-reexport.ts deleted file mode 100644 index 09256f3a51..0000000000 --- a/packages/opencode/script/unwrap-and-self-reexport.ts +++ /dev/null @@ -1,246 +0,0 @@ -#!/usr/bin/env bun -/** - * Unwrap a single `export namespace` in a file into flat top-level exports - * plus a self-reexport at the bottom of the same file. - * - * Usage: - * - * bun script/unwrap-and-self-reexport.ts src/file/ignore.ts - * bun script/unwrap-and-self-reexport.ts src/file/ignore.ts --dry-run - * - * Input file shape: - * - * // imports ... - * - * export namespace FileIgnore { - * export function ...(...) { ... } - * const helper = ... - * } - * - * Output shape: - * - * // imports ... - * - * export function ...(...) { ... } - * const helper = ... - * - * export * as FileIgnore from "./ignore" - * - * What the script does: - * - * 1. Uses ast-grep to locate the single `export namespace Foo { ... }` block. - * 2. Removes the `export namespace Foo {` line and the matching closing `}`. - * 3. Dedents the body by one indent level (2 spaces). - * 4. Rewrites `Foo.Bar` self-references inside the file to just `Bar` - * (but only for names that are actually exported from the namespace — - * non-exported members get the same treatment so references remain valid). - * 5. Appends `export * as Foo from "./"` at the end of the file. - * - * What it does NOT do: - * - * - Does not create or modify barrel `index.ts` files. - * - Does not rewrite any consumer imports. Consumers already import from - * the file path itself (e.g. `import { FileIgnore } from "../file/ignore"`); - * the self-reexport keeps that import working unchanged. - * - Does not handle files with more than one `export namespace` declaration. - * The script refuses that case. - * - * Requires: ast-grep (`brew install ast-grep`). - */ - -import fs from "node:fs" -import path from "node:path" - -const args = process.argv.slice(2) -const dryRun = args.includes("--dry-run") -const targetArg = args.find((a) => !a.startsWith("--")) - -if (!targetArg) { - console.error("Usage: bun script/unwrap-and-self-reexport.ts [--dry-run]") - process.exit(1) -} - -const absPath = path.resolve(targetArg) -if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) { - console.error(`Not a file: ${absPath}`) - process.exit(1) -} - -// Locate the namespace block with ast-grep (accurate AST boundaries). -const ast = Bun.spawnSync( - ["ast-grep", "run", "--pattern", "export namespace $NAME { $$$BODY }", "--lang", "typescript", "--json", absPath], - { stdout: "pipe", stderr: "pipe" }, -) -if (ast.exitCode !== 0) { - console.error("ast-grep failed:", ast.stderr.toString()) - process.exit(1) -} - -type AstMatch = { - range: { start: { line: number; column: number }; end: { line: number; column: number } } - metaVariables: { single: Record } -} -const matches = JSON.parse(ast.stdout.toString()) as AstMatch[] -if (matches.length === 0) { - console.error(`No \`export namespace\` found in ${path.relative(process.cwd(), absPath)}`) - process.exit(1) -} -if (matches.length > 1) { - console.error(`File has ${matches.length} \`export namespace\` declarations — this script handles one per file.`) - for (const m of matches) console.error(` ${m.metaVariables.single.NAME.text} (line ${m.range.start.line + 1})`) - process.exit(1) -} - -const match = matches[0] -const nsName = match.metaVariables.single.NAME.text -const startLine = match.range.start.line -const endLine = match.range.end.line - -const original = fs.readFileSync(absPath, "utf-8") -const lines = original.split("\n") - -// Split the file into before/body/after. -const before = lines.slice(0, startLine) -const body = lines.slice(startLine + 1, endLine) -const after = lines.slice(endLine + 1) - -// Dedent body by one indent level (2 spaces). -const dedented = body.map((line) => { - if (line === "") return "" - if (line.startsWith(" ")) return line.slice(2) - return line -}) - -// Collect all top-level declared identifiers inside the namespace body so we can -// rewrite `Foo.X` → `X` when X is one of them. We gather BOTH exported and -// non-exported names because the namespace body might reference its own -// non-exported helpers via `Foo.helper` too. -const declaredNames = new Set() -const declRe = - /^\s*(?:export\s+)?(?:abstract\s+)?(?:async\s+)?(?:const|let|var|function|class|interface|type|enum)\s+(\w+)/ -for (const line of dedented) { - const m = line.match(declRe) - if (m) declaredNames.add(m[1]) -} -// Also capture `export { X, Y }` re-exports inside the namespace. -const reExportRe = /export\s*\{\s*([^}]+)\}/g -for (const line of dedented) { - for (const reExport of line.matchAll(reExportRe)) { - for (const part of reExport[1].split(",")) { - const name = part - .trim() - .split(/\s+as\s+/) - .pop()! - .trim() - if (name) declaredNames.add(name) - } - } -} - -// Rewrite `Foo.X` → `X` inside the body, avoiding matches in strings, comments, -// templates. We walk the line char-by-char rather than using a regex so we can -// skip over those segments cleanly. -let rewriteCount = 0 -function rewriteLine(line: string): string { - const out: string[] = [] - let i = 0 - let stringQuote: string | null = null - while (i < line.length) { - const ch = line[i] - // String / template literal pass-through. - if (stringQuote) { - out.push(ch) - if (ch === "\\" && i + 1 < line.length) { - out.push(line[i + 1]) - i += 2 - continue - } - if (ch === stringQuote) stringQuote = null - i++ - continue - } - if (ch === '"' || ch === "'" || ch === "`") { - stringQuote = ch - out.push(ch) - i++ - continue - } - // Line comment: emit the rest of the line untouched. - if (ch === "/" && line[i + 1] === "/") { - out.push(line.slice(i)) - i = line.length - continue - } - // Block comment: emit until "*/" if present on same line; else rest of line. - if (ch === "/" && line[i + 1] === "*") { - const end = line.indexOf("*/", i + 2) - if (end === -1) { - out.push(line.slice(i)) - i = line.length - } else { - out.push(line.slice(i, end + 2)) - i = end + 2 - } - continue - } - // Try to match `Foo.` at this position. - if (line.startsWith(nsName + ".", i)) { - // Make sure the char before is NOT a word character (otherwise we'd be in the middle of another identifier). - const prev = i === 0 ? "" : line[i - 1] - if (!/\w/.test(prev)) { - const after = line.slice(i + nsName.length + 1) - const nameMatch = after.match(/^([A-Za-z_$][\w$]*)/) - if (nameMatch && declaredNames.has(nameMatch[1])) { - out.push(nameMatch[1]) - i += nsName.length + 1 + nameMatch[1].length - rewriteCount++ - continue - } - } - } - out.push(ch) - i++ - } - return out.join("") -} -const rewrittenBody = dedented.map(rewriteLine) - -// Assemble the new file. Collapse multiple trailing blank lines so the -// self-reexport sits cleanly at the end. -// -// When the file is itself `index.ts`, prefer `"."` over `"./index"` — both are -// valid but `"."` matches the existing convention in the codebase (e.g. -// pty/index.ts, file/index.ts, etc.) and avoids referencing "index" literally. -const basename = path.basename(absPath, ".ts") -const reexportSource = basename === "index" ? "." : `./${basename}` -const assembled = [...before, ...rewrittenBody, ...after].join("\n") -const trimmed = assembled.replace(/\s+$/g, "") -const output = `${trimmed}\n\nexport * as ${nsName} from "${reexportSource}"\n` - -if (dryRun) { - console.log(`--- dry run: ${path.relative(process.cwd(), absPath)} ---`) - console.log(`namespace: ${nsName}`) - console.log(`body lines: ${body.length}`) - console.log(`declared names: ${Array.from(declaredNames).join(", ") || "(none)"}`) - console.log(`self-refs rewr: ${rewriteCount}`) - console.log(`self-reexport: export * as ${nsName} from "${reexportSource}"`) - console.log(`output preview (last 10 lines):`) - const outputLines = output.split("\n") - for (const l of outputLines.slice(Math.max(0, outputLines.length - 10))) { - console.log(` ${l}`) - } - process.exit(0) -} - -fs.writeFileSync(absPath, output) -console.log(`unwrapped ${path.relative(process.cwd(), absPath)} → ${nsName}`) -console.log(` body lines: ${body.length}`) -console.log(` self-refs rewr: ${rewriteCount}`) -console.log(` self-reexport: export * as ${nsName} from "${reexportSource}"`) -console.log("") -console.log("Next: verify with") -console.log(" bunx --bun tsgo --noEmit") -console.log(" bun run --conditions=browser ./src/index.ts generate") -console.log( - ` bun run test test/${path.relative(path.join(path.dirname(absPath), "..", ".."), absPath).replace(/\.ts$/, "")}*`, -) diff --git a/packages/opencode/script/unwrap-namespace.ts b/packages/opencode/script/unwrap-namespace.ts deleted file mode 100644 index 45c16f6c73..0000000000 --- a/packages/opencode/script/unwrap-namespace.ts +++ /dev/null @@ -1,305 +0,0 @@ -#!/usr/bin/env bun -/** - * Unwrap a TypeScript `export namespace` into flat exports + barrel. - * - * Usage: - * bun script/unwrap-namespace.ts src/bus/index.ts - * bun script/unwrap-namespace.ts src/bus/index.ts --dry-run - * bun script/unwrap-namespace.ts src/pty/index.ts --name service # avoid collision with pty.ts - * - * What it does: - * 1. Reads the file and finds the `export namespace Foo { ... }` block - * (uses ast-grep for accurate AST-based boundary detection) - * 2. Removes the namespace wrapper and dedents the body - * 3. Fixes self-references (e.g. Config.PermissionAction → PermissionAction) - * 4. If the file is index.ts, renames it to .ts - * 5. Creates/updates index.ts with `export * as Foo from "./"` - * 6. Rewrites import paths across src/, test/, and script/ - * 7. Fixes sibling imports within the same directory - * - * Requires: ast-grep (`brew install ast-grep` or `cargo install ast-grep`) - */ - -import path from "path" -import fs from "fs" - -const args = process.argv.slice(2) -const dryRun = args.includes("--dry-run") -const nameFlag = args.find((a, i) => args[i - 1] === "--name") -const filePath = args.find((a) => !a.startsWith("--") && args[args.indexOf(a) - 1] !== "--name") - -if (!filePath) { - console.error("Usage: bun script/unwrap-namespace.ts [--dry-run] [--name ]") - process.exit(1) -} - -const absPath = path.resolve(filePath) -if (!fs.existsSync(absPath)) { - console.error(`File not found: ${absPath}`) - process.exit(1) -} - -const src = fs.readFileSync(absPath, "utf-8") -const lines = src.split("\n") - -// Use ast-grep to find the namespace boundaries accurately. -// This avoids false matches from braces in strings, templates, comments, etc. -const astResult = Bun.spawnSync( - ["ast-grep", "run", "--pattern", "export namespace $NAME { $$$BODY }", "--lang", "typescript", "--json", absPath], - { stdout: "pipe", stderr: "pipe" }, -) - -if (astResult.exitCode !== 0) { - console.error("ast-grep failed:", astResult.stderr.toString()) - process.exit(1) -} - -const matches = JSON.parse(astResult.stdout.toString()) as Array<{ - text: string - range: { start: { line: number; column: number }; end: { line: number; column: number } } - metaVariables: { single: Record; multi: Record> } -}> - -if (matches.length === 0) { - console.error("No `export namespace Foo { ... }` found in file") - process.exit(1) -} - -if (matches.length > 1) { - console.error(`Found ${matches.length} namespaces — this script handles one at a time`) - console.error("Namespaces found:") - for (const m of matches) console.error(` ${m.metaVariables.single.NAME.text} (line ${m.range.start.line + 1})`) - process.exit(1) -} - -const match = matches[0] -const nsName = match.metaVariables.single.NAME.text -const nsLine = match.range.start.line // 0-indexed -const closeLine = match.range.end.line // 0-indexed, the line with closing `}` - -console.log(`Found: export namespace ${nsName} { ... }`) -console.log(` Lines ${nsLine + 1}–${closeLine + 1} (${closeLine - nsLine + 1} lines)`) - -// Build the new file content: -// 1. Everything before the namespace declaration (imports, etc.) -// 2. The namespace body, dedented by one level (2 spaces) -// 3. Everything after the closing brace (rare, but possible) -const before = lines.slice(0, nsLine) -const body = lines.slice(nsLine + 1, closeLine) -const after = lines.slice(closeLine + 1) - -// Dedent: remove exactly 2 leading spaces from each line -const dedented = body.map((line) => { - if (line === "") return "" - if (line.startsWith(" ")) return line.slice(2) - return line -}) - -let newContent = [...before, ...dedented, ...after].join("\n") - -// --- Fix self-references --- -// After unwrapping, references like `Config.PermissionAction` inside the same file -// need to become just `PermissionAction`. Only fix code positions, not strings. -const exportedNames = new Set() -const exportRegex = /export\s+(?:const|function|class|interface|type|enum|abstract\s+class)\s+(\w+)/g -for (const line of dedented) { - for (const m of line.matchAll(exportRegex)) exportedNames.add(m[1]) -} -const reExportRegex = /export\s*\{\s*([^}]+)\}/g -for (const line of dedented) { - for (const m of line.matchAll(reExportRegex)) { - for (const name of m[1].split(",")) { - const trimmed = name - .trim() - .split(/\s+as\s+/) - .pop()! - .trim() - if (trimmed) exportedNames.add(trimmed) - } - } -} - -let selfRefCount = 0 -if (exportedNames.size > 0) { - const fixedLines = newContent.split("\n").map((line) => { - // Split line into string-literal and code segments to avoid replacing inside strings - const segments: Array<{ text: string; isString: boolean }> = [] - let i = 0 - let current = "" - let inString: string | null = null - - while (i < line.length) { - const ch = line[i] - if (inString) { - current += ch - if (ch === "\\" && i + 1 < line.length) { - current += line[i + 1] - i += 2 - continue - } - if (ch === inString) { - segments.push({ text: current, isString: true }) - current = "" - inString = null - } - i++ - continue - } - if (ch === '"' || ch === "'" || ch === "`") { - if (current) segments.push({ text: current, isString: false }) - current = ch - inString = ch - i++ - continue - } - if (ch === "/" && i + 1 < line.length && line[i + 1] === "/") { - current += line.slice(i) - segments.push({ text: current, isString: true }) - current = "" - i = line.length - continue - } - current += ch - i++ - } - if (current) segments.push({ text: current, isString: !!inString }) - - return segments - .map((seg) => { - if (seg.isString) return seg.text - let result = seg.text - for (const name of exportedNames) { - const pattern = `${nsName}.${name}` - while (result.includes(pattern)) { - const idx = result.indexOf(pattern) - const charBefore = idx > 0 ? result[idx - 1] : " " - const charAfter = idx + pattern.length < result.length ? result[idx + pattern.length] : " " - if (/\w/.test(charBefore) || /\w/.test(charAfter)) break - result = result.slice(0, idx) + name + result.slice(idx + pattern.length) - selfRefCount++ - } - } - return result - }) - .join("") - }) - newContent = fixedLines.join("\n") -} - -// Figure out file naming -const dir = path.dirname(absPath) -const basename = path.basename(absPath, ".ts") -const isIndex = basename === "index" -const implName = nameFlag ?? (isIndex ? nsName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase() : basename) -const implFile = path.join(dir, `${implName}.ts`) -const indexFile = path.join(dir, "index.ts") -const barrelLine = `export * as ${nsName} from "./${implName}"\n` - -console.log("") -if (isIndex) { - console.log(`Plan: rename ${basename}.ts → ${implName}.ts, create new index.ts barrel`) -} else { - console.log(`Plan: rewrite ${basename}.ts in place, create index.ts barrel`) -} -if (selfRefCount > 0) console.log(`Fixed ${selfRefCount} self-reference(s) (${nsName}.X → X)`) -console.log("") - -if (dryRun) { - console.log("--- DRY RUN ---") - console.log("") - console.log(`=== ${implName}.ts (first 30 lines) ===`) - newContent - .split("\n") - .slice(0, 30) - .forEach((l, i) => console.log(` ${i + 1}: ${l}`)) - console.log(" ...") - console.log("") - console.log(`=== index.ts ===`) - console.log(` ${barrelLine.trim()}`) - console.log("") - if (!isIndex) { - const relDir = path.relative(path.resolve("src"), dir) - console.log(`=== Import rewrites (would apply) ===`) - console.log(` ${relDir}/${basename}" → ${relDir}" across src/, test/, script/`) - } else { - console.log("No import rewrites needed (was index.ts)") - } -} else { - if (isIndex) { - fs.writeFileSync(implFile, newContent) - fs.writeFileSync(indexFile, barrelLine) - console.log(`Wrote ${implName}.ts (${newContent.split("\n").length} lines)`) - console.log(`Wrote index.ts (barrel)`) - } else { - fs.writeFileSync(absPath, newContent) - if (fs.existsSync(indexFile)) { - const existing = fs.readFileSync(indexFile, "utf-8") - if (!existing.includes(`export * as ${nsName}`)) { - fs.appendFileSync(indexFile, barrelLine) - console.log(`Appended to existing index.ts`) - } else { - console.log(`index.ts already has ${nsName} export`) - } - } else { - fs.writeFileSync(indexFile, barrelLine) - console.log(`Wrote index.ts (barrel)`) - } - console.log(`Rewrote ${basename}.ts (${newContent.split("\n").length} lines)`) - } - - // --- Rewrite import paths across src/, test/, script/ --- - const relDir = path.relative(path.resolve("src"), dir) - if (!isIndex) { - const oldTail = `${relDir}/${basename}` - const searchDirs = ["src", "test", "script"].filter((d) => fs.existsSync(d)) - const rgResult = Bun.spawnSync(["rg", "-l", `from.*${oldTail}"`, ...searchDirs], { - stdout: "pipe", - stderr: "pipe", - }) - const filesToRewrite = rgResult.stdout - .toString() - .trim() - .split("\n") - .filter((f) => f.length > 0) - - if (filesToRewrite.length > 0) { - console.log(`\nRewriting imports in ${filesToRewrite.length} file(s)...`) - for (const file of filesToRewrite) { - const content = fs.readFileSync(file, "utf-8") - fs.writeFileSync(file, content.replaceAll(`${oldTail}"`, `${relDir}"`)) - } - console.log(` Done: ${oldTail}" → ${relDir}"`) - } else { - console.log("\nNo import rewrites needed") - } - } else { - console.log("\nNo import rewrites needed (was index.ts)") - } - - // --- Fix sibling imports within the same directory --- - const siblingFiles = fs.readdirSync(dir).filter((f) => { - if (!f.endsWith(".ts")) return false - if (f === "index.ts" || f === `${implName}.ts`) return false - return true - }) - - let siblingFixCount = 0 - for (const sibFile of siblingFiles) { - const sibPath = path.join(dir, sibFile) - const content = fs.readFileSync(sibPath, "utf-8") - const pattern = new RegExp(`from\\s+["']\\./${basename}["']`, "g") - if (pattern.test(content)) { - fs.writeFileSync(sibPath, content.replace(pattern, `from "."`)) - siblingFixCount++ - } - } - if (siblingFixCount > 0) { - console.log(`Fixed ${siblingFixCount} sibling import(s) in ${path.basename(dir)}/ (./${basename} → .)`) - } -} - -console.log("") -console.log("=== Verify ===") -console.log("") -console.log("bunx --bun tsgo --noEmit # typecheck") -console.log("bun run test # run tests") diff --git a/packages/opencode/specs/effect/namespace-treeshake.md b/packages/opencode/specs/effect/namespace-treeshake.md deleted file mode 100644 index ef78c762bb..0000000000 --- a/packages/opencode/specs/effect/namespace-treeshake.md +++ /dev/null @@ -1,256 +0,0 @@ -# Namespace → self-reexport migration - -Migrate every `export namespace Foo { ... }` to flat top-level exports plus a -single self-reexport line at the bottom of the same file: - -```ts -export * as Foo from "./foo" -``` - -No barrel `index.ts` files. No cross-directory indirection. Consumers keep the -exact same `import { Foo } from "../foo/foo"` ergonomics. - -## Why this pattern - -We tested three options against Bun, esbuild, Rollup (what Vite uses under the -hood), Bun's runtime, and Node's native TypeScript runner. - -``` - heavy.ts loaded? - A. namespace B. barrel C. self-reexport -Bun bundler YES YES no -esbuild YES YES no -Rollup (Vite) YES YES no -Bun runtime YES YES no -Node --experimental-strip-types SYNTAX ERROR YES no -``` - -- **`export namespace`** compiles to an IIFE. Bundlers see one opaque function - call and can't analyze what's used. Node's native TS runner rejects the - syntax outright: `SyntaxError: TypeScript namespace declaration is not -supported in strip-only mode`. -- **Barrel `index.ts`** files (`export * as Foo from "./foo"` in a separate - file) force every re-exported sibling to evaluate when you import one name. - Siblings with side effects (top-level imports of SDKs, etc.) always load. -- **Self-reexport** keeps the file as plain ESM. Bundlers see static named - exports. The module is only pulled in when something actually imports from - it. There is no barrel hop, so no sibling contamination and no circular - import hazard. - -Bundle overhead for the self-reexport wrapper is roughly 240 bytes per module -(`Object.defineProperty` namespace proxy). At ~100 modules that's ~24KB — -negligible for a CLI binary. - -## The pattern - -### Before - -```ts -// src/permission/arity.ts -export namespace BashArity { - export function prefix(tokens: string[]) { ... } -} -``` - -### After - -```ts -// src/permission/arity.ts -export function prefix(tokens: string[]) { ... } - -export * as BashArity from "./arity" -``` - -Consumers don't change at all: - -```ts -import { BashArity } from "@/permission/arity" -BashArity.prefix(...) // still works -``` - -Editors still auto-import `BashArity` like any named export, because the file -does have a named `BashArity` export at the module top level. - -### Odd but harmless - -`BashArity.BashArity.BashArity.prefix(...)` compiles and runs because the -namespace contains a re-export of itself. Nobody would write that. Not a -problem. - -## Why this is different from what we tried first - -An earlier pass used sibling barrel files (`index.ts` with `export * as ...`). -That turned out to be wrong for our constraints: - -1. The barrel file always loads all its sibling modules when you import - through it, even if you only need one. For our CLI this is exactly the - cost we're trying to avoid. -2. Barrel + sibling imports made it very easy to accidentally create circular - imports that only surface as `ReferenceError` at runtime, not at - typecheck. - -The self-reexport has none of those issues. There is no indirection. The -file and the namespace are the same unit. - -## Why this matters for startup - -The worst import chain in the codebase looks like: - -``` -src/index.ts - └── FormatError from src/cli/error.ts - ├── { Provider } from provider/provider.ts (~1700 lines) - │ ├── 20+ @ai-sdk/* packages - │ ├── @aws-sdk/credential-providers - │ ├── google-auth-library - │ └── more - ├── { Config } from config/config.ts (~1600 lines) - └── { MCP } from mcp/mcp.ts (~900 lines) -``` - -All of that currently gets pulled in just to do `.isInstance()` on a handful -of error classes. The namespace IIFE shape is the main reason bundlers cannot -strip the unused parts. Self-reexport + flat ESM fixes it. - -## Automation - -From `packages/opencode`: - -```bash -bun script/unwrap-namespace.ts [--dry-run] -``` - -The script: - -1. Uses ast-grep to locate the `export namespace Foo { ... }` block accurately. -2. Removes the `export namespace Foo {` line and the matching closing `}`. -3. Dedents the body by one indent level (2 spaces). -4. Rewrites `Foo.Bar` self-references inside the file to just `Bar`. -5. Appends `export * as Foo from "./"` at the bottom of the file. -6. Never creates a barrel `index.ts`. - -### Typical flow for one file - -```bash -# 1. Preview -bun script/unwrap-namespace.ts src/permission/arity.ts --dry-run - -# 2. Apply -bun script/unwrap-namespace.ts src/permission/arity.ts - -# 3. Verify -cd packages/opencode -bunx --bun tsgo --noEmit -bun run --conditions=browser ./src/index.ts generate -bun run test -``` - -### Consumer imports usually don't need to change - -Most consumers already import straight from the file, e.g.: - -```ts -import { BashArity } from "@/permission/arity" -import { Config } from "@/config/config" -``` - -Because the file itself now does `export * as Foo from "./foo"`, those imports -keep working with zero edits. - -The only edits needed are when a consumer was importing through a previous -barrel (`"@/config"` or `"../config"` resolving to `config/index.ts`). In -that case, repoint it at the file: - -```ts -// before -import { Config } from "@/config" - -// after -import { Config } from "@/config/config" -``` - -### Dynamic imports in tests - -If a test did `const { Foo } = await import("../../src/x/y")`, the destructure -still works because of the self-reexport. No change required. - -## Verification checklist (per PR) - -Run all of these locally before pushing: - -```bash -cd packages/opencode -bunx --bun tsgo --noEmit -bun run --conditions=browser ./src/index.ts generate -bun run test -``` - -Also do a quick grep in `src/`, `test/`, and `script/` to make sure no -consumer is still importing the namespace from an old barrel path that no -longer exports it. - -The SDK build step (`bun run --conditions=browser ./src/index.ts generate`) -evaluates every module eagerly and is the most reliable way to catch circular -import regressions at runtime — the typechecker does not catch these. - -## Rules for new code - -- No new `export namespace`. -- Every module directory has a single canonical file — typically - `dir/index.ts` — with flat top-level exports and a self-reexport at the - bottom: - `export * as Foo from "."` -- Consumers import from the directory: - `import { Foo } from "@/dir"` or `import { Foo } from "../dir"`. -- No sibling barrel files. If a directory has multiple independent - namespaces, they each get their own file (e.g. `config/config.ts`, - `config/plugin.ts`) and their own self-reexport; the `index.ts` in that - directory stays minimal or does not exist. -- If a file needs a sibling, import the sibling file directly: - `import * as Sibling from "./sibling"`, not `from "."`. - -### Why `dir/index.ts` + `"."` is fine for us - -A single-file module (e.g. `pty/`) can live entirely in `dir/index.ts` -with `export * as Foo from "."` at the bottom. Consumers write the -short form: - -```ts -import { Pty } from "@/pty" -``` - -This works in Bun runtime, Bun build, esbuild, and Rollup. It does NOT -work under Node's `--experimental-strip-types` runner: - -``` -node --experimental-strip-types entry.ts - ERR_UNSUPPORTED_DIR_IMPORT: Directory import '/.../pty' is not supported -``` - -Node requires an explicit file or a `package.json#exports` map for ESM. -We don't care about that target right now because the opencode CLI is -built with Bun and the web apps are built with Vite/Rollup. If we ever -want to run raw `.ts` through Node, we'll need to either use explicit -`.ts` extensions everywhere or add per-directory `package.json` exports -maps. - -### When NOT to collapse to `index.ts` - -Some directories contain multiple independent namespaces where -`dir/index.ts` would be misleading. Examples: - -- `config/` has `Config`, `ConfigPaths`, `ConfigMarkdown`, `ConfigPlugin`, - `ConfigKeybinds`. Each lives in its own file with its own self-reexport - (`config/config.ts`, `config/plugin.ts`, etc.). Consumers import the - specific one: `import { ConfigPlugin } from "@/config/plugin"`. -- Same shape for `session/`, `server/`, etc. - -Collapsing one of those into `index.ts` would mean picking a single -"canonical" namespace for the directory, which breaks the symmetry and -hides the other files. - -## Scope - -There are still dozens of `export namespace` files left across the codebase. -Each one is its own small PR. Do them one at a time, verified locally, rather -than batching by directory. From ee7339f2c6a4575a81bcaff2240076571db03e11 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 23:10:45 -0400 Subject: [PATCH 060/335] refactor: move provider and config provider routes onto HttpApi (#23004) --- packages/opencode/src/provider/auth.ts | 38 +-- packages/opencode/src/provider/provider.ts | 228 ++++++++++-------- .../opencode/src/server/instance/config.ts | 12 +- .../src/server/instance/httpapi/config.ts | 51 ++++ .../src/server/instance/httpapi/provider.ts | 104 +++++++- .../src/server/instance/httpapi/server.ts | 3 + .../opencode/src/server/instance/index.ts | 19 +- .../opencode/src/server/instance/provider.ts | 26 +- 8 files changed, 323 insertions(+), 158 deletions(-) create mode 100644 packages/opencode/src/server/instance/httpapi/config.ts diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index c0c73b2cc1..5d8b2765de 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -58,6 +58,18 @@ export class Authorization extends Schema.Class("ProviderAuthAuth static readonly zod = zod(this) } +export const AuthorizeInput = Schema.Struct({ + method: Schema.Number.annotate({ description: "Auth method index" }), + inputs: Schema.optional(Schema.Record(Schema.String, Schema.String)).annotate({ description: "Prompt inputs" }), +}).pipe(withStatics((s) => ({ zod: zod(s) }))) +export type AuthorizeInput = Schema.Schema.Type + +export const CallbackInput = Schema.Struct({ + method: Schema.Number.annotate({ description: "Auth method index" }), + code: Schema.optional(Schema.String).annotate({ description: "OAuth authorization code" }), +}).pipe(withStatics((s) => ({ zod: zod(s) }))) +export type CallbackInput = Schema.Schema.Type + export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod })) export const OauthCodeMissing = NamedError.create( @@ -86,12 +98,12 @@ type Hook = NonNullable export interface Interface { readonly methods: () => Effect.Effect - readonly authorize: (input: { - providerID: ProviderID - method: number - inputs?: Record - }) => Effect.Effect - readonly callback: (input: { providerID: ProviderID; method: number; code?: string }) => Effect.Effect + readonly authorize: ( + input: { + providerID: ProviderID + } & AuthorizeInput, + ) => Effect.Effect + readonly callback: (input: { providerID: ProviderID } & CallbackInput) => Effect.Effect } interface State { @@ -153,11 +165,9 @@ export const layer: Layer.Layer = ) }) - const authorize = Effect.fn("ProviderAuth.authorize")(function* (input: { - providerID: ProviderID - method: number - inputs?: Record - }) { + const authorize = Effect.fn("ProviderAuth.authorize")(function* ( + input: { providerID: ProviderID } & AuthorizeInput, + ) { const { hooks, pending } = yield* InstanceState.get(state) const method = hooks[input.providerID].methods[input.method] if (method.type !== "oauth") return @@ -180,11 +190,7 @@ export const layer: Layer.Layer = } }) - const callback = Effect.fn("ProviderAuth.callback")(function* (input: { - providerID: ProviderID - method: number - code?: string - }) { + const callback = Effect.fn("ProviderAuth.callback")(function* (input: { providerID: ProviderID } & CallbackInput) { const pending = (yield* InstanceState.get(state)).pending const match = pending.get(input.providerID) if (!match) return yield* Effect.fail(new OauthMissing({ providerID: input.providerID })) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index a7297634e7..711481d80a 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -16,14 +16,16 @@ import { Env } from "../env" import { Instance } from "../project/instance" import { InstallationVersion } from "../installation/version" import { Flag } from "../flag/flag" +import { zod } from "@/util/effect-zod" import { iife } from "@/util/iife" import { Global } from "../global" import path from "path" -import { Effect, Layer, Context } from "effect" +import { Effect, Layer, Context, Schema, Types } from "effect" import { EffectBridge } from "@/effect" import { InstanceState } from "@/effect" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { isRecord } from "@/util/record" +import { withStatics } from "@/util/schema" import * as ProviderTransform from "./transform" import { ModelID, ProviderID } from "./schema" @@ -796,91 +798,111 @@ function custom(dep: CustomDep): Record { } } -export const Model = z - .object({ - id: ModelID.zod, - providerID: ProviderID.zod, - api: z.object({ - id: z.string(), - url: z.string(), - npm: z.string(), - }), - name: z.string(), - family: z.string().optional(), - capabilities: z.object({ - temperature: z.boolean(), - reasoning: z.boolean(), - attachment: z.boolean(), - toolcall: z.boolean(), - input: z.object({ - text: z.boolean(), - audio: z.boolean(), - image: z.boolean(), - video: z.boolean(), - pdf: z.boolean(), - }), - output: z.object({ - text: z.boolean(), - audio: z.boolean(), - image: z.boolean(), - video: z.boolean(), - pdf: z.boolean(), - }), - interleaved: z.union([ - z.boolean(), - z.object({ - field: z.enum(["reasoning_content", "reasoning_details"]), - }), - ]), - }), - cost: z.object({ - input: z.number(), - output: z.number(), - cache: z.object({ - read: z.number(), - write: z.number(), - }), - experimentalOver200K: z - .object({ - input: z.number(), - output: z.number(), - cache: z.object({ - read: z.number(), - write: z.number(), - }), - }) - .optional(), - }), - limit: z.object({ - context: z.number(), - input: z.number().optional(), - output: z.number(), - }), - status: z.enum(["alpha", "beta", "deprecated", "active"]), - options: z.record(z.string(), z.any()), - headers: z.record(z.string(), z.string()), - release_date: z.string(), - variants: z.record(z.string(), z.record(z.string(), z.any())).optional(), - }) - .meta({ - ref: "Model", - }) -export type Model = z.infer +const ProviderApiInfo = Schema.Struct({ + id: Schema.String, + url: Schema.String, + npm: Schema.String, +}) -export const Info = z - .object({ - id: ProviderID.zod, - name: z.string(), - source: z.enum(["env", "config", "custom", "api"]), - env: z.string().array(), - key: z.string().optional(), - options: z.record(z.string(), z.any()), - models: z.record(z.string(), Model), - }) - .meta({ - ref: "Provider", - }) -export type Info = z.infer +const ProviderModalities = Schema.Struct({ + text: Schema.Boolean, + audio: Schema.Boolean, + image: Schema.Boolean, + video: Schema.Boolean, + pdf: Schema.Boolean, +}) + +const ProviderInterleaved = Schema.Union([ + Schema.Boolean, + Schema.Struct({ + field: Schema.Literals(["reasoning_content", "reasoning_details"]), + }), +]) + +const ProviderCapabilities = Schema.Struct({ + temperature: Schema.Boolean, + reasoning: Schema.Boolean, + attachment: Schema.Boolean, + toolcall: Schema.Boolean, + input: ProviderModalities, + output: ProviderModalities, + interleaved: ProviderInterleaved, +}) + +const ProviderCacheCost = Schema.Struct({ + read: Schema.Number, + write: Schema.Number, +}) + +const ProviderCost = Schema.Struct({ + input: Schema.Number, + output: Schema.Number, + cache: ProviderCacheCost, + experimentalOver200K: Schema.optional( + Schema.Struct({ + input: Schema.Number, + output: Schema.Number, + cache: ProviderCacheCost, + }), + ), +}) + +const ProviderLimit = Schema.Struct({ + context: Schema.Number, + input: Schema.optional(Schema.Number), + output: Schema.Number, +}) + +export const Model = Schema.Struct({ + id: ModelID, + providerID: ProviderID, + api: ProviderApiInfo, + name: Schema.String, + family: Schema.optional(Schema.String), + capabilities: ProviderCapabilities, + cost: ProviderCost, + limit: ProviderLimit, + status: Schema.Literals(["alpha", "beta", "deprecated", "active"]), + options: Schema.Record(Schema.String, Schema.Any), + headers: Schema.Record(Schema.String, Schema.String), + release_date: Schema.String, + variants: Schema.optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Any))), +}) + .annotate({ identifier: "Model" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Model = Types.DeepMutable> + +export const Info = Schema.Struct({ + id: ProviderID, + name: Schema.String, + source: Schema.Literals(["env", "config", "custom", "api"]), + env: Schema.Array(Schema.String), + key: Schema.optional(Schema.String), + options: Schema.Record(Schema.String, Schema.Any), + models: Schema.Record(Schema.String, Model), +}) + .annotate({ identifier: "Provider" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Info = Types.DeepMutable> + +const DefaultModelIDs = Schema.Record(Schema.String, Schema.String) + +export const ListResult = Schema.Struct({ + all: Schema.Array(Info), + default: DefaultModelIDs, + connected: Schema.Array(Schema.String), +}).pipe(withStatics((s) => ({ zod: zod(s) }))) +export type ListResult = Types.DeepMutable> + +export const ConfigProvidersResult = Schema.Struct({ + providers: Schema.Array(Info), + default: DefaultModelIDs, +}).pipe(withStatics((s) => ({ zod: zod(s) }))) +export type ConfigProvidersResult = Types.DeepMutable> + +export function defaultModelIDs }>(providers: Record) { + return mapValues(providers, (item) => sort(Object.values(item.models))[0].id) +} export interface Interface { readonly list: () => Effect.Effect> @@ -928,7 +950,7 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] { } function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { - const m: Model = { + const base: Model = { id: ModelID.make(model.id), providerID: ProviderID.make(provider.id), name: model.name, @@ -972,9 +994,10 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model variants: {}, } - m.variants = mapValues(ProviderTransform.variants(m), (v) => v) - - return m + return { + ...base, + variants: mapValues(ProviderTransform.variants(base), (v) => v), + } } export function fromModelsDevProvider(provider: ModelsDev.Provider): Info { @@ -983,17 +1006,22 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info { models[key] = fromModelsDevModel(provider, model) for (const [mode, opts] of Object.entries(model.experimental?.modes ?? {})) { const id = `${model.id}-${mode}` - const m = fromModelsDevModel(provider, model) - m.id = ModelID.make(id) - m.name = `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}` - if (opts.cost) m.cost = mergeDeep(m.cost, cost(opts.cost)) - // convert body params to camelCase for ai sdk compatibility - if (opts.provider?.body) - m.options = Object.fromEntries( - Object.entries(opts.provider.body).map(([k, v]) => [k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()), v]), - ) - if (opts.provider?.headers) m.headers = opts.provider.headers - models[id] = m + const base = fromModelsDevModel(provider, model) + models[id] = { + ...base, + id: ModelID.make(id), + name: `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`, + cost: opts.cost ? mergeDeep(base.cost, cost(opts.cost)) : base.cost, + options: opts.provider?.body + ? Object.fromEntries( + Object.entries(opts.provider.body).map(([k, v]) => [ + k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()), + v, + ]), + ) + : base.options, + headers: opts.provider?.headers ?? base.headers, + } } } return { diff --git a/packages/opencode/src/server/instance/config.ts b/packages/opencode/src/server/instance/config.ts index e3291a8c36..15c393fe5a 100644 --- a/packages/opencode/src/server/instance/config.ts +++ b/packages/opencode/src/server/instance/config.ts @@ -3,7 +3,6 @@ import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" import { Config } from "../../config" import { Provider } from "../../provider" -import { mapValues } from "remeda" import { errors } from "../error" import { lazy } from "../../util/lazy" import { AppRuntime } from "../../effect/app-runtime" @@ -70,12 +69,7 @@ export const ConfigRoutes = lazy(() => description: "List of providers", content: { "application/json": { - schema: resolver( - z.object({ - providers: Provider.Info.array(), - default: z.record(z.string(), z.string()), - }), - ), + schema: resolver(Provider.ConfigProvidersResult.zod), }, }, }, @@ -84,10 +78,10 @@ export const ConfigRoutes = lazy(() => async (c) => jsonRequest("ConfigRoutes.providers", c, function* () { const svc = yield* Provider.Service - const providers = mapValues(yield* svc.list(), (item) => item) + const providers = yield* svc.list() return { providers: Object.values(providers), - default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id), + default: Provider.defaultModelIDs(providers), } }), ), diff --git a/packages/opencode/src/server/instance/httpapi/config.ts b/packages/opencode/src/server/instance/httpapi/config.ts new file mode 100644 index 0000000000..14aa94f9fc --- /dev/null +++ b/packages/opencode/src/server/instance/httpapi/config.ts @@ -0,0 +1,51 @@ +import { Config } from "@/config" +import { Provider } from "@/provider" +import { Effect, Layer } from "effect" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" + +const root = "/config" + +export const ConfigApi = HttpApi.make("config") + .add( + HttpApiGroup.make("config") + .add( + HttpApiEndpoint.get("providers", `${root}/providers`, { + success: Provider.ConfigProvidersResult, + }).annotateMerge( + OpenApi.annotations({ + identifier: "config.providers", + summary: "List config providers", + description: "Get a list of all configured AI providers and their default models.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "config", + description: "Experimental HttpApi config routes.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "opencode experimental HttpApi", + version: "0.0.1", + description: "Experimental HttpApi surface for selected instance routes.", + }), + ) + +export const configHandlers = Layer.unwrap( + Effect.gen(function* () { + const svc = yield* Provider.Service + + const providers = Effect.fn("ConfigHttpApi.providers")(function* () { + const providers = yield* svc.list() + return { + providers: Object.values(providers), + default: Provider.defaultModelIDs(providers), + } + }) + + return HttpApiBuilder.group(ConfigApi, "config", (handlers) => handlers.handle("providers", providers)) + }), +).pipe(Layer.provide(Provider.defaultLayer), Layer.provide(Config.defaultLayer)) diff --git a/packages/opencode/src/server/instance/httpapi/provider.ts b/packages/opencode/src/server/instance/httpapi/provider.ts index 31dd1446a0..67831a1faf 100644 --- a/packages/opencode/src/server/instance/httpapi/provider.ts +++ b/packages/opencode/src/server/instance/httpapi/provider.ts @@ -1,6 +1,11 @@ import { ProviderAuth } from "@/provider" -import { Effect, Layer } from "effect" -import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Config } from "@/config" +import { ModelsDev } from "@/provider" +import { Provider } from "@/provider" +import { ProviderID } from "@/provider/schema" +import { mapValues } from "remeda" +import { Effect, Layer, Schema } from "effect" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" const root = "/provider" @@ -8,6 +13,15 @@ export const ProviderApi = HttpApi.make("provider") .add( HttpApiGroup.make("provider") .add( + HttpApiEndpoint.get("list", root, { + success: Provider.ListResult, + }).annotateMerge( + OpenApi.annotations({ + identifier: "provider.list", + summary: "List providers", + description: "Get a list of all available AI providers, including both available and connected ones.", + }), + ), HttpApiEndpoint.get("auth", `${root}/auth`, { success: ProviderAuth.Methods, }).annotateMerge( @@ -17,6 +31,28 @@ export const ProviderApi = HttpApi.make("provider") description: "Retrieve available authentication methods for all AI providers.", }), ), + HttpApiEndpoint.post("authorize", `${root}/:providerID/oauth/authorize`, { + params: { providerID: ProviderID }, + payload: ProviderAuth.AuthorizeInput, + success: ProviderAuth.Authorization, + }).annotateMerge( + OpenApi.annotations({ + identifier: "provider.oauth.authorize", + summary: "Start OAuth authorization", + description: "Start the OAuth authorization flow for a provider.", + }), + ), + HttpApiEndpoint.post("callback", `${root}/:providerID/oauth/callback`, { + params: { providerID: ProviderID }, + payload: ProviderAuth.CallbackInput, + success: Schema.Boolean, + }).annotateMerge( + OpenApi.annotations({ + identifier: "provider.oauth.callback", + summary: "Handle OAuth callback", + description: "Handle the OAuth callback from a provider after user authorization.", + }), + ), ) .annotateMerge( OpenApi.annotations({ @@ -35,12 +71,72 @@ export const ProviderApi = HttpApi.make("provider") export const providerHandlers = Layer.unwrap( Effect.gen(function* () { + const cfg = yield* Config.Service + const provider = yield* Provider.Service const svc = yield* ProviderAuth.Service + const list = Effect.fn("ProviderHttpApi.list")(function* () { + const config = yield* cfg.get() + const all = yield* Effect.promise(() => ModelsDev.get()) + const disabled = new Set(config.disabled_providers ?? []) + const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined + const filtered: Record = {} + for (const [key, value] of Object.entries(all)) { + if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) { + filtered[key] = value + } + } + const connected = yield* provider.list() + const providers = Object.assign( + mapValues(filtered, (item) => Provider.fromModelsDevProvider(item)), + connected, + ) + return { + all: Object.values(providers), + default: Provider.defaultModelIDs(providers), + connected: Object.keys(connected), + } + }) + const auth = Effect.fn("ProviderHttpApi.auth")(function* () { return yield* svc.methods() }) - return HttpApiBuilder.group(ProviderApi, "provider", (handlers) => handlers.handle("auth", auth)) + const authorize = Effect.fn("ProviderHttpApi.authorize")(function* (ctx: { + params: { providerID: ProviderID } + payload: ProviderAuth.AuthorizeInput + }) { + const result = yield* svc + .authorize({ + providerID: ctx.params.providerID, + method: ctx.payload.method, + inputs: ctx.payload.inputs, + }) + .pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({})))) + if (!result) return yield* new HttpApiError.BadRequest({}) + return result + }) + + const callback = Effect.fn("ProviderHttpApi.callback")(function* (ctx: { + params: { providerID: ProviderID } + payload: ProviderAuth.CallbackInput + }) { + yield* svc + .callback({ + providerID: ctx.params.providerID, + method: ctx.payload.method, + code: ctx.payload.code, + }) + .pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({})))) + return true + }) + + return HttpApiBuilder.group(ProviderApi, "provider", (handlers) => + handlers.handle("list", list).handle("auth", auth).handle("authorize", authorize).handle("callback", callback), + ) }), -).pipe(Layer.provide(ProviderAuth.defaultLayer)) +).pipe( + Layer.provide(ProviderAuth.defaultLayer), + Layer.provide(Provider.defaultLayer), + Layer.provide(Config.defaultLayer), +) diff --git a/packages/opencode/src/server/instance/httpapi/server.ts b/packages/opencode/src/server/instance/httpapi/server.ts index 362d0970b9..64332fd2a0 100644 --- a/packages/opencode/src/server/instance/httpapi/server.ts +++ b/packages/opencode/src/server/instance/httpapi/server.ts @@ -10,6 +10,7 @@ import { InstanceBootstrap } from "@/project/bootstrap" import { Instance } from "@/project/instance" import { lazy } from "@/util/lazy" import { Filesystem } from "@/util" +import { ConfigApi, configHandlers } from "./config" import { PermissionApi, permissionHandlers } from "./permission" import { ProviderApi, providerHandlers } from "./provider" import { QuestionApi, questionHandlers } from "./question" @@ -108,8 +109,10 @@ const instance = HttpRouter.middleware()( const QuestionSecured = QuestionApi.middleware(Authorization) const PermissionSecured = PermissionApi.middleware(Authorization) const ProviderSecured = ProviderApi.middleware(Authorization) +const ConfigSecured = ConfigApi.middleware(Authorization) export const routes = Layer.mergeAll( + HttpApiBuilder.layer(ConfigSecured).pipe(Layer.provide(configHandlers)), HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)), HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)), HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)), diff --git a/packages/opencode/src/server/instance/index.ts b/packages/opencode/src/server/instance/index.ts index 9ef6da63ac..6a290093c5 100644 --- a/packages/opencode/src/server/instance/index.ts +++ b/packages/opencode/src/server/instance/index.ts @@ -1,7 +1,7 @@ import { describeRoute, resolver, validator } from "hono-openapi" import { Hono } from "hono" import type { UpgradeWebSocket } from "hono/ws" -import { Effect } from "effect" +import { Context, Effect } from "effect" import z from "zod" import { Format } from "../../format" import { TuiRoutes } from "./tui" @@ -41,12 +41,17 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) { const handler = ExperimentalHttpApiServer.webHandler().handler - app - .all("/question", (c) => handler(c.req.raw)) - .all("/question/*", (c) => handler(c.req.raw)) - .all("/permission", (c) => handler(c.req.raw)) - .all("/permission/*", (c) => handler(c.req.raw)) - .all("/provider/auth", (c) => handler(c.req.raw)) + const context = Context.empty() as Context.Context + app.get("/question", (c) => handler(c.req.raw, context)) + app.post("/question/:requestID/reply", (c) => handler(c.req.raw, context)) + app.post("/question/:requestID/reject", (c) => handler(c.req.raw, context)) + app.get("/permission", (c) => handler(c.req.raw, context)) + app.post("/permission/:requestID/reply", (c) => handler(c.req.raw, context)) + app.get("/config/providers", (c) => handler(c.req.raw, context)) + app.get("/provider", (c) => handler(c.req.raw, context)) + app.get("/provider/auth", (c) => handler(c.req.raw, context)) + app.post("/provider/:providerID/oauth/authorize", (c) => handler(c.req.raw, context)) + app.post("/provider/:providerID/oauth/callback", (c) => handler(c.req.raw, context)) } return app diff --git a/packages/opencode/src/server/instance/provider.ts b/packages/opencode/src/server/instance/provider.ts index c1580437da..a81ae00d59 100644 --- a/packages/opencode/src/server/instance/provider.ts +++ b/packages/opencode/src/server/instance/provider.ts @@ -25,13 +25,7 @@ export const ProviderRoutes = lazy(() => description: "List of providers", content: { "application/json": { - schema: resolver( - z.object({ - all: Provider.Info.array(), - default: z.record(z.string(), z.string()), - connected: z.array(z.string()), - }), - ), + schema: resolver(Provider.ListResult.zod), }, }, }, @@ -59,7 +53,7 @@ export const ProviderRoutes = lazy(() => ) return { all: Object.values(providers), - default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id), + default: Provider.defaultModelIDs(providers), connected: Object.keys(connected), } }), @@ -116,13 +110,7 @@ export const ProviderRoutes = lazy(() => providerID: ProviderID.zod.meta({ description: "Provider ID" }), }), ), - validator( - "json", - z.object({ - method: z.number().meta({ description: "Auth method index" }), - inputs: z.record(z.string(), z.string()).optional().meta({ description: "Prompt inputs" }), - }), - ), + validator("json", ProviderAuth.AuthorizeInput.zod), async (c) => { const providerID = c.req.valid("param").providerID const { method, inputs } = c.req.valid("json") @@ -162,13 +150,7 @@ export const ProviderRoutes = lazy(() => providerID: ProviderID.zod.meta({ description: "Provider ID" }), }), ), - validator( - "json", - z.object({ - method: z.number().meta({ description: "Auth method index" }), - code: z.string().optional().meta({ description: "OAuth authorization code" }), - }), - ), + validator("json", ProviderAuth.CallbackInput.zod), async (c) => { const providerID = c.req.valid("param").providerID const { method, code } = c.req.valid("json") From cccb907a9b3df7eb6fae71ee9e2392dccc73e9d3 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 23:19:18 -0400 Subject: [PATCH 061/335] feat(tui): animated GO logo + radial pulse in free-limit upsell dialog (#22976) --- .../src/cli/cmd/tui/component/bg-pulse.tsx | 130 +++++++ .../cmd/tui/component/dialog-go-upsell.tsx | 150 +++++--- .../src/cli/cmd/tui/component/logo.tsx | 335 ++++++++++++++---- .../cli/cmd/tui/component/shimmer-config.ts | 49 +++ packages/opencode/src/cli/logo.ts | 7 +- 5 files changed, 563 insertions(+), 108 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/component/bg-pulse.tsx create mode 100644 packages/opencode/src/cli/cmd/tui/component/shimmer-config.ts diff --git a/packages/opencode/src/cli/cmd/tui/component/bg-pulse.tsx b/packages/opencode/src/cli/cmd/tui/component/bg-pulse.tsx new file mode 100644 index 0000000000..541ecea4e1 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/bg-pulse.tsx @@ -0,0 +1,130 @@ +import { BoxRenderable, RGBA } from "@opentui/core" +import { createMemo, createSignal, For, onCleanup, onMount } from "solid-js" +import { tint, useTheme } from "@tui/context/theme" + +const PERIOD = 4600 +const RINGS = 3 +const WIDTH = 3.8 +const TAIL = 9.5 +const AMP = 0.55 +const TAIL_AMP = 0.16 +const BREATH_AMP = 0.05 +const BREATH_SPEED = 0.0008 +// Offset so bg ring emits from GO center at the moment the logo pulse peaks. +const PHASE_OFFSET = 0.29 + +export type BgPulseMask = { + x: number + y: number + width: number + height: number + pad?: number + strength?: number +} + +export function BgPulse(props: { centerX?: number; centerY?: number; masks?: BgPulseMask[] }) { + const { theme } = useTheme() + const [now, setNow] = createSignal(performance.now()) + const [size, setSize] = createSignal<{ width: number; height: number }>({ width: 0, height: 0 }) + let box: BoxRenderable | undefined + + const timer = setInterval(() => setNow(performance.now()), 50) + onCleanup(() => clearInterval(timer)) + + const sync = () => { + if (!box) return + setSize({ width: box.width, height: box.height }) + } + + onMount(() => { + sync() + box?.on("resize", sync) + }) + + onCleanup(() => { + box?.off("resize", sync) + }) + + const grid = createMemo(() => { + const t = now() + const w = size().width + const h = size().height + if (w === 0 || h === 0) return [] as RGBA[][] + const cxv = props.centerX ?? w / 2 + const cyv = props.centerY ?? h / 2 + const reach = Math.hypot(Math.max(cxv, w - cxv), Math.max(cyv, h - cyv) * 2) + TAIL + const ringStates = Array.from({ length: RINGS }, (_, i) => { + const offset = i / RINGS + const phase = (t / PERIOD + offset - PHASE_OFFSET + 1) % 1 + const envelope = Math.sin(phase * Math.PI) + const eased = envelope * envelope * (3 - 2 * envelope) + return { + head: phase * reach, + eased, + } + }) + const normalizedMasks = props.masks?.map((m) => { + const pad = m.pad ?? 2 + return { + left: m.x - pad, + right: m.x + m.width + pad, + top: m.y - pad, + bottom: m.y + m.height + pad, + pad, + strength: m.strength ?? 0.85, + } + }) + const rows = [] as RGBA[][] + for (let y = 0; y < h; y++) { + const row = [] as RGBA[] + for (let x = 0; x < w; x++) { + const dx = x + 0.5 - cxv + const dy = (y + 0.5 - cyv) * 2 + const dist = Math.hypot(dx, dy) + let level = 0 + for (const ring of ringStates) { + const delta = dist - ring.head + const crest = Math.abs(delta) < WIDTH ? 0.5 + 0.5 * Math.cos((delta / WIDTH) * Math.PI) : 0 + const tail = delta < 0 && delta > -TAIL ? (1 + delta / TAIL) ** 2.3 : 0 + level += (crest * AMP + tail * TAIL_AMP) * ring.eased + } + const edgeFalloff = Math.max(0, 1 - (dist / (reach * 0.85)) ** 2) + const breath = (0.5 + 0.5 * Math.sin(t * BREATH_SPEED)) * BREATH_AMP + let maskAtten = 1 + if (normalizedMasks) { + for (const m of normalizedMasks) { + if (x < m.left || x > m.right || y < m.top || y > m.bottom) continue + const inX = Math.min(x - m.left, m.right - x) + const inY = Math.min(y - m.top, m.bottom - y) + const edge = Math.min(inX / m.pad, inY / m.pad, 1) + const eased = edge * edge * (3 - 2 * edge) + const reduce = 1 - m.strength * eased + if (reduce < maskAtten) maskAtten = reduce + } + } + const strength = Math.min(1, ((level / RINGS) * edgeFalloff + breath * edgeFalloff) * maskAtten) + row.push(tint(theme.backgroundPanel, theme.primary, strength * 0.7)) + } + rows.push(row) + } + return rows + }) + + return ( + (box = item)} width="100%" height="100%"> + + {(row) => ( + + + {(color) => ( + + {" "} + + )} + + + )} + + + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-go-upsell.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-go-upsell.tsx index 2d200ca3b8..ace4b090bc 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-go-upsell.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-go-upsell.tsx @@ -1,12 +1,16 @@ -import { RGBA, TextAttributes } from "@opentui/core" +import { BoxRenderable, RGBA, TextAttributes } from "@opentui/core" import { useKeyboard } from "@opentui/solid" import open from "open" -import { createSignal } from "solid-js" +import { createSignal, onCleanup, onMount } from "solid-js" import { selectedForeground, useTheme } from "@tui/context/theme" import { useDialog, type DialogContext } from "@tui/ui/dialog" import { Link } from "@tui/ui/link" +import { GoLogo } from "./logo" +import { BgPulse, type BgPulseMask } from "./bg-pulse" const GO_URL = "https://opencode.ai/go" +const PAD_X = 3 +const PAD_TOP_OUTER = 1 export type DialogGoUpsellProps = { onClose?: (dontShowAgain?: boolean) => void @@ -27,62 +31,116 @@ export function DialogGoUpsell(props: DialogGoUpsellProps) { const dialog = useDialog() const { theme } = useTheme() const fg = selectedForeground(theme) - const [selected, setSelected] = createSignal(0) + const [selected, setSelected] = createSignal<"dismiss" | "subscribe">("subscribe") + const [center, setCenter] = createSignal<{ x: number; y: number } | undefined>() + const [masks, setMasks] = createSignal([]) + let content: BoxRenderable | undefined + let logoBox: BoxRenderable | undefined + let headingBox: BoxRenderable | undefined + let descBox: BoxRenderable | undefined + let buttonsBox: BoxRenderable | undefined + + const sync = () => { + if (!content || !logoBox) return + setCenter({ + x: logoBox.x - content.x + logoBox.width / 2, + y: logoBox.y - content.y + logoBox.height / 2 + PAD_TOP_OUTER, + }) + const next: BgPulseMask[] = [] + const baseY = PAD_TOP_OUTER + for (const b of [headingBox, descBox, buttonsBox]) { + if (!b) continue + next.push({ + x: b.x - content.x, + y: b.y - content.y + baseY, + width: b.width, + height: b.height, + pad: 2, + strength: 0.78, + }) + } + setMasks(next) + } + + onMount(() => { + sync() + for (const b of [content, logoBox, headingBox, descBox, buttonsBox]) b?.on("resize", sync) + }) + + onCleanup(() => { + for (const b of [content, logoBox, headingBox, descBox, buttonsBox]) b?.off("resize", sync) + }) useKeyboard((evt) => { if (evt.name === "left" || evt.name === "right" || evt.name === "tab") { - setSelected((s) => (s === 0 ? 1 : 0)) + setSelected((s) => (s === "subscribe" ? "dismiss" : "subscribe")) return } - if (evt.name !== "return") return - if (selected() === 0) subscribe(props, dialog) - else dismiss(props, dialog) + if (evt.name === "return") { + if (selected() === "subscribe") subscribe(props, dialog) + else dismiss(props, dialog) + } }) return ( - - - - Free limit reached - - dialog.clear()}> - esc - + (content = item)}> + + - - - Subscribe to OpenCode Go to keep going with reliable access to the best open-source models, starting at - $5/month. - - + + (headingBox = item)} flexDirection="row" justifyContent="space-between"> + + Free limit reached + + dialog.clear()}> + esc + + + (descBox = item)} gap={0}> + + Subscribe to + + OpenCode Go + + for reliable access to the + + best open-source models, starting at $5/month. + + + (logoBox = item)}> + + - - - setSelected(0)} - onMouseUp={() => subscribe(props, dialog)} - > - - subscribe - - - setSelected(1)} - onMouseUp={() => dismiss(props, dialog)} - > - (buttonsBox = item)} flexDirection="row" justifyContent="space-between"> + setSelected("dismiss")} + onMouseUp={() => dismiss(props, dialog)} > - don't show again - + + don't show again + + + setSelected("subscribe")} + onMouseUp={() => subscribe(props, dialog)} + > + + subscribe + + diff --git a/packages/opencode/src/cli/cmd/tui/component/logo.tsx b/packages/opencode/src/cli/cmd/tui/component/logo.tsx index e53974871a..17368ddad8 100644 --- a/packages/opencode/src/cli/cmd/tui/component/logo.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/logo.tsx @@ -1,8 +1,14 @@ import { BoxRenderable, MouseButton, MouseEvent, RGBA, TextAttributes } from "@opentui/core" -import { For, createMemo, createSignal, onCleanup, type JSX } from "solid-js" +import { For, createMemo, createSignal, onCleanup, onMount, type JSX } from "solid-js" import { useTheme, tint } from "@tui/context/theme" import * as Sound from "@tui/util/sound" -import { logo } from "@/cli/logo" +import { go, logo } from "@/cli/logo" +import { shimmerConfig, type ShimmerConfig } from "./shimmer-config" + +export type LogoShape = { + left: string[] + right: string[] +} // Shadow markers (rendered chars in parens): // _ = full shadow cell (space with bg=shadow) @@ -74,9 +80,6 @@ type Frame = { spark: number } -const LEFT = logo.left[0]?.length ?? 0 -const FULL = logo.left.map((line, i) => line + " ".repeat(GAP) + logo.right[i]) -const SPAN = Math.hypot(FULL[0]?.length ?? 0, FULL.length * 2) * 0.94 const NEAR = [ [1, 0], [1, 1], @@ -140,7 +143,7 @@ function noise(x: number, y: number, t: number) { } function lit(char: string) { - return char !== " " && char !== "_" && char !== "~" + return char !== " " && char !== "_" && char !== "~" && char !== "," } function key(x: number, y: number) { @@ -188,12 +191,12 @@ function route(list: Array<{ x: number; y: number }>) { return path } -function mapGlyphs() { +function mapGlyphs(full: string[]) { const cells = [] as Array<{ x: number; y: number }> - for (let y = 0; y < FULL.length; y++) { - for (let x = 0; x < (FULL[y]?.length ?? 0); x++) { - if (lit(FULL[y]?.[x] ?? " ")) cells.push({ x, y }) + for (let y = 0; y < full.length; y++) { + for (let x = 0; x < (full[y]?.length ?? 0); x++) { + if (lit(full[y]?.[x] ?? " ")) cells.push({ x, y }) } } @@ -237,9 +240,25 @@ function mapGlyphs() { return { glyph, trace, center } } -const MAP = mapGlyphs() +type LogoContext = { + LEFT: number + FULL: string[] + SPAN: number + MAP: ReturnType + shape: LogoShape +} -function shimmer(x: number, y: number, frame: Frame) { +function build(shape: LogoShape): LogoContext { + const LEFT = shape.left[0]?.length ?? 0 + const FULL = shape.left.map((line, i) => line + " ".repeat(GAP) + shape.right[i]) + const SPAN = Math.hypot(FULL[0]?.length ?? 0, FULL.length * 2) * 0.94 + return { LEFT, FULL, SPAN, MAP: mapGlyphs(FULL), shape } +} + +const DEFAULT = build(logo) +const GO = build(go) + +function shimmer(x: number, y: number, frame: Frame, ctx: LogoContext) { return frame.list.reduce((best, item) => { const age = frame.t - item.at if (age < SHIMMER_IN || age > LIFE) return best @@ -247,7 +266,7 @@ function shimmer(x: number, y: number, frame: Frame) { const dy = y * 2 + 1 - item.y const dist = Math.hypot(dx, dy) const p = age / LIFE - const r = SPAN * (1 - (1 - p) ** EXPAND) + const r = ctx.SPAN * (1 - (1 - p) ** EXPAND) const lag = r - dist if (lag < 0.18 || lag > SHIMMER_OUT) return best const band = Math.exp(-(((lag - 1.05) / 0.68) ** 2)) @@ -258,19 +277,19 @@ function shimmer(x: number, y: number, frame: Frame) { }, 0) } -function remain(x: number, y: number, item: Release, t: number) { +function remain(x: number, y: number, item: Release, t: number, ctx: LogoContext) { const age = t - item.at if (age < 0 || age > LIFE) return 0 const p = age / LIFE const dx = x + 0.5 - item.x - 0.5 const dy = y * 2 + 1 - item.y * 2 - 1 const dist = Math.hypot(dx, dy) - const r = SPAN * (1 - (1 - p) ** EXPAND) + const r = ctx.SPAN * (1 - (1 - p) ** EXPAND) if (dist > r) return 1 return clamp((r - dist) / 1.35 < 1 ? 1 - (r - dist) / 1.35 : 0) } -function wave(x: number, y: number, frame: Frame, live: boolean) { +function wave(x: number, y: number, frame: Frame, live: boolean, ctx: LogoContext) { return frame.list.reduce((sum, item) => { const age = frame.t - item.at if (age < 0 || age > LIFE) return sum @@ -278,7 +297,7 @@ function wave(x: number, y: number, frame: Frame, live: boolean) { const dx = x + 0.5 - item.x const dy = y * 2 + 1 - item.y const dist = Math.hypot(dx, dy) - const r = SPAN * (1 - (1 - p) ** EXPAND) + const r = ctx.SPAN * (1 - (1 - p) ** EXPAND) const fade = (1 - p) ** 1.32 const j = 1.02 + noise(x + item.x * 0.7, y + item.y * 0.7, item.at * 0.002 + age * 0.06) * 0.52 const edge = Math.exp(-(((dist - r) / WIDTH) ** 2)) * GAIN * fade * item.force * j @@ -292,7 +311,7 @@ function wave(x: number, y: number, frame: Frame, live: boolean) { }, 0) } -function field(x: number, y: number, frame: Frame) { +function field(x: number, y: number, frame: Frame, ctx: LogoContext) { const held = frame.hold const rest = frame.release const item = held ?? rest @@ -326,11 +345,11 @@ function field(x: number, y: number, frame: Frame) { Math.max(0, noise(item.x * 3.1, item.y * 2.7, frame.t * 1.7) - 0.72) * Math.exp(-(dist * dist) / 0.15) * lerp(0.08, 0.42, body) - const fade = frame.release && !frame.hold ? remain(x, y, frame.release, frame.t) : 1 + const fade = frame.release && !frame.hold ? remain(x, y, frame.release, frame.t, ctx) : 1 return (core + shell + ember + ring + fork + glitch + lash + flicker - dim) * fade } -function pick(x: number, y: number, frame: Frame) { +function pick(x: number, y: number, frame: Frame, ctx: LogoContext) { const held = frame.hold const rest = frame.release const item = held ?? rest @@ -339,26 +358,26 @@ function pick(x: number, y: number, frame: Frame) { const dx = x + 0.5 - item.x - 0.5 const dy = y * 2 + 1 - item.y * 2 - 1 const dist = Math.hypot(dx, dy) - const fade = frame.release && !frame.hold ? remain(x, y, frame.release, frame.t) : 1 + const fade = frame.release && !frame.hold ? remain(x, y, frame.release, frame.t, ctx) : 1 return Math.exp(-(dist * dist) / 1.7) * lerp(0.2, 0.96, rise) * fade } -function select(x: number, y: number) { - const direct = MAP.glyph.get(key(x, y)) +function select(x: number, y: number, ctx: LogoContext) { + const direct = ctx.MAP.glyph.get(key(x, y)) if (direct !== undefined) return direct - const near = NEAR.map(([dx, dy]) => MAP.glyph.get(key(x + dx, y + dy))).find( + const near = NEAR.map(([dx, dy]) => ctx.MAP.glyph.get(key(x + dx, y + dy))).find( (item): item is number => item !== undefined, ) return near } -function trace(x: number, y: number, frame: Frame) { +function trace(x: number, y: number, frame: Frame, ctx: LogoContext) { const held = frame.hold const rest = frame.release const item = held ?? rest if (!item || item.glyph === undefined) return 0 - const step = MAP.trace.get(key(x, y)) + const step = ctx.MAP.trace.get(key(x, y)) if (!step || step.glyph !== item.glyph || step.l < 2) return 0 const age = frame.t - item.at const rise = held ? ramp(age, HOLD, CHARGE) : rest!.rise @@ -368,29 +387,125 @@ function trace(x: number, y: number, frame: Frame) { const dist = Math.min(Math.abs(step.i - head), step.l - Math.abs(step.i - head)) const tail = (head - TAIL + step.l) % step.l const lag = Math.min(Math.abs(step.i - tail), step.l - Math.abs(step.i - tail)) - const fade = frame.release && !frame.hold ? remain(x, y, frame.release, frame.t) : 1 + const fade = frame.release && !frame.hold ? remain(x, y, frame.release, frame.t, ctx) : 1 const core = Math.exp(-((dist / 1.05) ** 2)) * lerp(0.8, 2.35, rise) const glow = Math.exp(-((dist / 1.85) ** 2)) * lerp(0.08, 0.34, rise) const trail = Math.exp(-((lag / 1.45) ** 2)) * lerp(0.04, 0.42, rise) return (core + glow + trail) * appear * fade } -function bloom(x: number, y: number, frame: Frame) { +function idle( + x: number, + pixelY: number, + frame: Frame, + ctx: LogoContext, + state: IdleState, +): { glow: number; peak: number; primary: number } { + const cfg = state.cfg + const dx = x + 0.5 - cfg.originX + const dy = pixelY - cfg.originY + const dist = Math.hypot(dx, dy) + const angle = Math.atan2(dy, dx) + const wob1 = noise(x * 0.32, pixelY * 0.25, frame.t * 0.0005) - 0.5 + const wob2 = noise(x * 0.12, pixelY * 0.08, frame.t * 0.00022) - 0.5 + const ripple = Math.sin(angle * 3 + frame.t * 0.0012) * 0.3 + const jitter = (wob1 * 0.55 + wob2 * 0.32 + ripple * 0.18) * cfg.noise + const traveled = dist + jitter + let glow = 0 + let peak = 0 + let halo = 0 + let primary = 0 + let ambient = 0 + for (const active of state.active) { + const head = active.head + const eased = active.eased + const delta = traveled - head + // Use shallower exponent (1.6 vs 2) for softer edges on the Gaussians + // so adjacent pixels have smaller brightness deltas + const core = Math.exp(-(Math.abs(delta / cfg.coreWidth) ** 1.8)) + const soft = Math.exp(-(Math.abs(delta / cfg.softWidth) ** 1.6)) + const tailRange = cfg.tail * 2.6 + const tail = delta < 0 && delta > -tailRange ? (1 + delta / tailRange) ** 2.6 : 0 + const haloDelta = delta + cfg.haloOffset + const haloBand = Math.exp(-(Math.abs(haloDelta / cfg.haloWidth) ** 1.6)) + glow += (soft * cfg.softAmp + tail * cfg.tailAmp) * eased + peak += core * cfg.coreAmp * eased + halo += haloBand * cfg.haloAmp * eased + // Primary-tinted fringe follows the halo (which trails behind the core) and the tail + primary += (haloBand + tail * 0.6) * eased + ambient += active.ambient + } + ambient /= state.rings + return { + glow: glow / state.rings, + peak: cfg.breathBase + ambient + (peak + halo) / state.rings, + primary: (primary / state.rings) * cfg.primaryMix, + } +} + +function bloom(x: number, y: number, frame: Frame, ctx: LogoContext) { const item = frame.glow if (!item) return 0 - const glyph = MAP.glyph.get(key(x, y)) + const glyph = ctx.MAP.glyph.get(key(x, y)) if (glyph !== item.glyph) return 0 const age = frame.t - item.at if (age < 0 || age > GLOW_OUT) return 0 const p = age / GLOW_OUT const flash = (1 - p) ** 2 - const dx = x + 0.5 - MAP.center.get(item.glyph)!.x - const dy = y * 2 + 1 - MAP.center.get(item.glyph)!.y + const dx = x + 0.5 - ctx.MAP.center.get(item.glyph)!.x + const dy = y * 2 + 1 - ctx.MAP.center.get(item.glyph)!.y const bias = Math.exp(-((Math.hypot(dx, dy) / 2.8) ** 2)) return lerp(item.force, item.force * 0.18, p) * lerp(0.72, 1.1, bias) * flash } -export function Logo() { +type IdleState = { + cfg: ShimmerConfig + reach: number + rings: number + active: Array<{ + head: number + eased: number + ambient: number + }> +} + +function buildIdleState(t: number, ctx: LogoContext): IdleState { + const cfg = shimmerConfig + const w = ctx.FULL[0]?.length ?? 1 + const h = ctx.FULL.length * 2 + const corners: [number, number][] = [ + [0, 0], + [w, 0], + [0, h], + [w, h], + ] + let maxCorner = 0 + for (const [cx, cy] of corners) { + const d = Math.hypot(cx - cfg.originX, cy - cfg.originY) + if (d > maxCorner) maxCorner = d + } + const reach = maxCorner + cfg.tail * 2 + const rings = Math.max(1, Math.floor(cfg.rings)) + const active = [] as IdleState["active"] + for (let i = 0; i < rings; i++) { + const offset = i / rings + const cyclePhase = (t / cfg.period + offset) % 1 + if (cyclePhase >= cfg.sweepFraction) continue + const phase = cyclePhase / cfg.sweepFraction + const envelope = Math.sin(phase * Math.PI) + const eased = envelope * envelope * (3 - 2 * envelope) + const d = (phase - cfg.ambientCenter) / cfg.ambientWidth + active.push({ + head: phase * reach, + eased, + ambient: Math.abs(d) < 1 ? (1 - d * d) ** 2 * cfg.ambientAmp : 0, + }) + } + return { cfg, reach, rings, active } +} + +export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } = {}) { + const ctx = props.shape ? build(props.shape) : DEFAULT const { theme } = useTheme() const [rings, setRings] = createSignal([]) const [hold, setHold] = createSignal() @@ -430,6 +545,7 @@ export function Logo() { } if (!live) setRelease(undefined) if (live || hold() || release() || glow()) return + if (props.idle) return stop() } @@ -438,8 +554,20 @@ export function Logo() { timer = setInterval(tick, 16) } + onCleanup(() => { + stop() + hum = false + Sound.dispose() + }) + + onMount(() => { + if (!props.idle) return + setNow(performance.now()) + start() + }) + const hit = (x: number, y: number) => { - const char = FULL[y]?.[x] + const char = ctx.FULL[y]?.[x] return char !== undefined && char !== " " } @@ -448,7 +576,7 @@ export function Logo() { if (last) burst(last.x, last.y) setNow(t) if (!last) setRelease(undefined) - setHold({ x, y, at: t, glyph: select(x, y) }) + setHold({ x, y, at: t, glyph: select(x, y, ctx) }) hum = false start() } @@ -508,6 +636,8 @@ export function Logo() { } }) + const idleState = createMemo(() => (props.idle ? buildIdleState(frame().t, ctx) : undefined)) + const renderLine = ( line: string, y: number, @@ -516,24 +646,64 @@ export function Logo() { off: number, frame: Frame, dusk: Frame, + state: IdleState | undefined, ): JSX.Element[] => { const shadow = tint(theme.background, ink, 0.25) const attrs = bold ? TextAttributes.BOLD : undefined return Array.from(line).map((char, i) => { - const h = field(off + i, y, frame) - const n = wave(off + i, y, frame, lit(char)) + h - const s = wave(off + i, y, dusk, false) + h - const p = lit(char) ? pick(off + i, y, frame) : 0 - const e = lit(char) ? trace(off + i, y, frame) : 0 - const b = lit(char) ? bloom(off + i, y, frame) : 0 - const q = shimmer(off + i, y, frame) + if (char === " ") { + return ( + + {char} + + ) + } + + const h = field(off + i, y, frame, ctx) + const charLit = lit(char) + // Sub-pixel sampling: cells are 2 pixels tall. Sample at top (y*2) and bottom (y*2+1) pixel rows. + const pulseTop = state ? idle(off + i, y * 2, frame, ctx, state) : { glow: 0, peak: 0, primary: 0 } + const pulseBot = state ? idle(off + i, y * 2 + 1, frame, ctx, state) : { glow: 0, peak: 0, primary: 0 } + const peakMixTop = charLit ? Math.min(1, pulseTop.peak) : 0 + const peakMixBot = charLit ? Math.min(1, pulseBot.peak) : 0 + const primaryMixTop = charLit ? Math.min(1, pulseTop.primary) : 0 + const primaryMixBot = charLit ? Math.min(1, pulseBot.primary) : 0 + // Layer primary tint first, then white peak on top — so the halo/tail pulls toward primary, + // while the bright core stays pure white + const inkTopTint = primaryMixTop > 0 ? tint(ink, theme.primary, primaryMixTop) : ink + const inkBotTint = primaryMixBot > 0 ? tint(ink, theme.primary, primaryMixBot) : ink + const inkTop = peakMixTop > 0 ? tint(inkTopTint, PEAK, peakMixTop) : inkTopTint + const inkBot = peakMixBot > 0 ? tint(inkBotTint, PEAK, peakMixBot) : inkBotTint + // For the non-peak-aware brightness channels, use the average of top/bot + const pulse = { + glow: (pulseTop.glow + pulseBot.glow) / 2, + peak: (pulseTop.peak + pulseBot.peak) / 2, + primary: (pulseTop.primary + pulseBot.primary) / 2, + } + const peakMix = charLit ? Math.min(1, pulse.peak) : 0 + const primaryMix = charLit ? Math.min(1, pulse.primary) : 0 + const inkPrimary = primaryMix > 0 ? tint(ink, theme.primary, primaryMix) : ink + const inkTinted = peakMix > 0 ? tint(inkPrimary, PEAK, peakMix) : inkPrimary + const shadowMixCfg = state?.cfg.shadowMix ?? shimmerConfig.shadowMix + const shadowMixTop = Math.min(1, pulseTop.peak * shadowMixCfg) + const shadowMixBot = Math.min(1, pulseBot.peak * shadowMixCfg) + const shadowTop = shadowMixTop > 0 ? tint(shadow, PEAK, shadowMixTop) : shadow + const shadowBot = shadowMixBot > 0 ? tint(shadow, PEAK, shadowMixBot) : shadow + const shadowMix = Math.min(1, pulse.peak * shadowMixCfg) + const shadowTinted = shadowMix > 0 ? tint(shadow, PEAK, shadowMix) : shadow + const n = wave(off + i, y, frame, charLit, ctx) + h + const s = wave(off + i, y, dusk, false, ctx) + h + const p = charLit ? pick(off + i, y, frame, ctx) : 0 + const e = charLit ? trace(off + i, y, frame, ctx) : 0 + const b = charLit ? bloom(off + i, y, frame, ctx) : 0 + const q = shimmer(off + i, y, frame, ctx) if (char === "_") { return ( @@ -545,8 +715,8 @@ export function Logo() { if (char === "^") { return ( @@ -557,34 +727,60 @@ export function Logo() { if (char === "~") { return ( - + ) } - if (char === " ") { + if (char === ",") { return ( - - {char} + + ▄ + + ) + } + + // Solid █: render as ▀ so the top pixel (fg) and bottom pixel (bg) can carry independent shimmer values + if (char === "█") { + return ( + + ▀ + + ) + } + + // ▀ top-half-lit: fg uses top-pixel sample, bg stays transparent/panel + if (char === "▀") { + return ( + + ▀ + + ) + } + + // ▄ bottom-half-lit: fg uses bottom-pixel sample + if (char === "▄") { + return ( + + ▄ ) } return ( - + {char} ) }) } - onCleanup(() => { - stop() - hum = false - Sound.dispose() - }) - const mouse = (evt: MouseEvent) => { if (!box) return if ((evt.type === "down" || evt.type === "drag") && evt.button === MouseButton.LEFT) { @@ -613,17 +809,28 @@ export function Logo() { position="absolute" top={0} left={0} - width={FULL[0]?.length ?? 0} - height={FULL.length} + width={ctx.FULL[0]?.length ?? 0} + height={ctx.FULL.length} zIndex={1} onMouse={mouse} /> - + {(line, index) => ( - {renderLine(line, index(), theme.textMuted, false, 0, frame(), dusk())} - {renderLine(logo.right[index()], index(), theme.text, true, LEFT + GAP, frame(), dusk())} + {renderLine(line, index(), props.ink ?? theme.textMuted, !!props.ink, 0, frame(), dusk(), idleState())} + + + {renderLine( + ctx.shape.right[index()], + index(), + props.ink ?? theme.text, + true, + ctx.LEFT + GAP, + frame(), + dusk(), + idleState(), + )} )} @@ -631,3 +838,9 @@ export function Logo() { ) } + +export function GoLogo() { + const { theme } = useTheme() + const base = tint(theme.background, theme.text, 0.62) + return +} diff --git a/packages/opencode/src/cli/cmd/tui/component/shimmer-config.ts b/packages/opencode/src/cli/cmd/tui/component/shimmer-config.ts new file mode 100644 index 0000000000..01bc136f5d --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/shimmer-config.ts @@ -0,0 +1,49 @@ +export type ShimmerConfig = { + period: number + rings: number + sweepFraction: number + coreWidth: number + coreAmp: number + softWidth: number + softAmp: number + tail: number + tailAmp: number + haloWidth: number + haloOffset: number + haloAmp: number + breathBase: number + noise: number + ambientAmp: number + ambientCenter: number + ambientWidth: number + shadowMix: number + primaryMix: number + originX: number + originY: number +} + +export const shimmerDefaults: ShimmerConfig = { + period: 4600, + rings: 2, + sweepFraction: 1, + coreWidth: 1.2, + coreAmp: 1.9, + softWidth: 10, + softAmp: 1.6, + tail: 5, + tailAmp: 0.64, + haloWidth: 4.3, + haloOffset: 0.6, + haloAmp: 0.16, + breathBase: 0.04, + noise: 0.1, + ambientAmp: 0.36, + ambientCenter: 0.5, + ambientWidth: 0.34, + shadowMix: 0.1, + primaryMix: 0.3, + originX: 4.5, + originY: 13.5, +} + +export const shimmerConfig: ShimmerConfig = { ...shimmerDefaults } diff --git a/packages/opencode/src/cli/logo.ts b/packages/opencode/src/cli/logo.ts index 44fb93c15b..a58a8cf995 100644 --- a/packages/opencode/src/cli/logo.ts +++ b/packages/opencode/src/cli/logo.ts @@ -3,4 +3,9 @@ export const logo = { right: [" ▄ ", "█▀▀▀ █▀▀█ █▀▀█ █▀▀█", "█___ █__█ █__█ █^^^", "▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"], } -export const marks = "_^~" +export const go = { + left: [" ", "█▀▀▀", "█_^█", "▀▀▀▀"], + right: [" ", "█▀▀█", "█__█", "▀▀▀▀"], +} + +export const marks = "_^~," From fbbab9d6c8a03c4cd5bed0d13a85f52e3aca47ce Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 16 Apr 2026 23:31:00 -0400 Subject: [PATCH 062/335] feat(app): hide desktop titlebar tools behind settings (#19029) Co-authored-by: Brendan Allan Co-authored-by: Brendan Allan --- .../src/components/session/session-header.tsx | 105 ++++++----- .../app/src/components/settings-general.tsx | 74 ++++++++ packages/app/src/components/titlebar.tsx | 8 +- packages/app/src/context/settings.tsx | 30 +++ packages/app/src/env.d.ts | 6 +- packages/app/src/i18n/en.ts | 11 ++ .../src/pages/session/session-side-panel.tsx | 176 +++++++++--------- .../pages/session/use-session-commands.tsx | 21 ++- 8 files changed, 290 insertions(+), 141 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 7acfdfc374..021e5be67e 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -8,7 +8,7 @@ import { Spinner } from "@opencode-ai/ui/spinner" import { showToast } from "@opencode-ai/ui/toast" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { getFilename } from "@opencode-ai/shared/util/path" -import { createEffect, createMemo, For, Show } from "solid-js" +import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js" import { createStore } from "solid-js/store" import { Portal } from "solid-js/web" import { useCommand } from "@/context/command" @@ -16,6 +16,7 @@ import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" import { usePlatform } from "@/context/platform" import { useServer } from "@/context/server" +import { useSettings } from "@/context/settings" import { useSync } from "@/context/sync" import { useTerminal } from "@/context/terminal" import { focusTerminalById } from "@/pages/session/helpers" @@ -134,6 +135,7 @@ export function SessionHeader() { const server = useServer() const platform = usePlatform() const language = useLanguage() + const settings = useSettings() const sync = useSync() const terminal = useTerminal() const { params, view } = useSessionLayout() @@ -151,6 +153,11 @@ export function SessionHeader() { }) const hotkey = createMemo(() => command.keybind("file.open")) const os = createMemo(() => detectOS(platform)) + const isDesktopBeta = platform.platform === "desktop" && import.meta.env.VITE_OPENCODE_CHANNEL === "beta" + const search = createMemo(() => !isDesktopBeta || settings.general.showSearch()) + const tree = createMemo(() => !isDesktopBeta || settings.general.showFileTree()) + const term = createMemo(() => !isDesktopBeta || settings.general.showTerminal()) + const status = createMemo(() => !isDesktopBeta || settings.general.showStatus()) const [exists, setExists] = createStore>>({ finder: true, @@ -262,12 +269,16 @@ export function SessionHeader() { .catch((err: unknown) => showRequestError(language, err)) } - const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center")) - const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right")) + const [centerMount, setCenterMount] = createSignal(null) + const [rightMount, setRightMount] = createSignal(null) + onMount(() => { + setCenterMount(document.getElementById("opencode-titlebar-center")) + setRightMount(document.getElementById("opencode-titlebar-right")) + }) return ( <> - + {(mount) => ( - + + + diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index b4ac061df4..c380fb69b3 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -106,6 +106,7 @@ export const SettingsGeneral: Component = () => { permission.disableAutoAccept(params.id, value) } + const desktop = createMemo(() => platform.platform === "desktop") const check = () => { if (!platform.checkUpdate) return @@ -279,6 +280,74 @@ export const SettingsGeneral: Component = () => { ) + const AdvancedSection = () => ( +
+

{language.t("settings.general.section.advanced")}

+ + + +
+ settings.general.setShowFileTree(checked)} + /> +
+
+ + +
+ settings.general.setShowNavigation(checked)} + /> +
+
+ + +
+ settings.general.setShowSearch(checked)} + /> +
+
+ + +
+ settings.general.setShowTerminal(checked)} + /> +
+
+ + +
+ settings.general.setShowStatus(checked)} + /> +
+
+
+
+ ) + const AppearanceSection = () => (

{language.t("settings.general.section.appearance")}

@@ -527,6 +596,7 @@ export const SettingsGeneral: Component = () => {
) + console.log(import.meta.env) return (
@@ -609,6 +679,10 @@ export const SettingsGeneral: Component = () => { ) }} + + + +
) diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index b7edb85ede..409fcbeff6 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -11,6 +11,7 @@ import { useLayout } from "@/context/layout" import { usePlatform } from "@/context/platform" import { useCommand } from "@/context/command" import { useLanguage } from "@/context/language" +import { useSettings } from "@/context/settings" import { applyPath, backPath, forwardPath } from "./titlebar-history" type TauriDesktopWindow = { @@ -40,6 +41,7 @@ export function Titlebar() { const platform = usePlatform() const command = useCommand() const language = useLanguage() + const settings = useSettings() const theme = useTheme() const navigate = useNavigate() const location = useLocation() @@ -78,6 +80,7 @@ export function Titlebar() { const canBack = createMemo(() => history.index > 0) const canForward = createMemo(() => history.index < history.stack.length - 1) const hasProjects = createMemo(() => layout.projects.list().length > 0) + const nav = createMemo(() => import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" || settings.general.showNavigation()) const back = () => { const next = backPath(history) @@ -255,13 +258,12 @@ export function Titlebar() {
- +
-
+
- - - - {props.reviewCount()}{" "} - {language.t( - props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other", - )} - - - {language.t("session.files.all")} - - - - - - - {language.t("common.loading")} - {language.t("common.loading.ellipsis")} -
- } - > + + + + {props.reviewCount()}{" "} + {language.t( + props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other", + )} + + + {language.t("session.files.all")} + + + + + + + {language.t("common.loading")} + {language.t("common.loading.ellipsis")} +
+ } + > + props.focusReviewDiff(node.path)} + /> +
+ + + + + + {empty(language.t("session.files.empty"))} + props.focusReviewDiff(node.path)} + onFileClick={(node) => openTab(file.tab(node.path))} /> - - - {empty(props.empty())} - - - - - {empty(language.t("session.files.empty"))} - - openTab(file.tab(node.path))} - /> - - - - - - -
props.size.start()}> - { - props.size.touch() - layout.fileTree.resize(width) - }} - /> + + + +
-
- + +
props.size.start()}> + { + props.size.touch() + layout.fileTree.resize(width) + }} + /> +
+
+ + diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index b5d2544636..9bbeb10bde 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -7,8 +7,10 @@ import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" import { useLocal } from "@/context/local" import { usePermission } from "@/context/permission" +import { usePlatform } from "@/context/platform" import { usePrompt } from "@/context/prompt" import { useSDK } from "@/context/sdk" +import { useSettings } from "@/context/settings" import { useSync } from "@/context/sync" import { useTerminal } from "@/context/terminal" import { showToast } from "@opencode-ai/ui/toast" @@ -39,8 +41,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const language = useLanguage() const local = useLocal() const permission = usePermission() + const platform = usePlatform() const prompt = usePrompt() const sdk = useSDK() + const settings = useSettings() const sync = useSync() const terminal = useTerminal() const layout = useLayout() @@ -66,6 +70,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { }) const activeFileTab = tabState.activeFileTab const closableTab = tabState.closableTab + const shown = () => platform.platform !== "desktop" || settings.general.showFileTree() const idle = { type: "idle" as const } const status = () => sync.data.session_status[params.id ?? ""] ?? idle @@ -457,12 +462,16 @@ export const useSessionCommands = (actions: SessionCommandContext) => { keybind: "mod+shift+r", onSelect: () => view().reviewPanel.toggle(), }), - viewCommand({ - id: "fileTree.toggle", - title: language.t("command.fileTree.toggle"), - keybind: "mod+\\", - onSelect: () => layout.fileTree.toggle(), - }), + ...(shown() + ? [ + viewCommand({ + id: "fileTree.toggle", + title: language.t("command.fileTree.toggle"), + keybind: "mod+\\", + onSelect: () => layout.fileTree.toggle(), + }), + ] + : []), viewCommand({ id: "input.focus", title: language.t("command.input.focus"), From 0bedea52b19515c69057866ec958769004147f66 Mon Sep 17 00:00:00 2001 From: James Long Date: Thu, 16 Apr 2026 23:35:36 -0400 Subject: [PATCH 063/335] fix(tui): tui resiliency when workspace is dead, disable directory filter in session list (#23013) --- .opencode/opencode.jsonc | 1 + bun.lock | 347 +++++++++++++++++- daytona.ts | 199 ++++++++++ package.json | 1 + .../src/cli/cmd/tui/context/project.tsx | 19 +- .../opencode/src/cli/cmd/tui/context/sync.tsx | 32 +- .../src/cli/cmd/tui/routes/session/index.tsx | 39 +- packages/opencode/src/session/session.ts | 7 +- .../test/cli/tui/sync-provider.test.tsx | 280 -------------- 9 files changed, 595 insertions(+), 330 deletions(-) create mode 100644 daytona.ts delete mode 100644 packages/opencode/test/cli/tui/sync-provider.test.tsx diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index 8380f7f719..91b7abdbc6 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -10,6 +10,7 @@ "packages/opencode/migration/*": "deny", }, }, + "plugin": ["../daytona.ts"], "mcp": {}, "tools": { "github-triage": false, diff --git a/bun.lock b/bun.lock index 63232cb29e..7c562c4d4b 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "opencode", "dependencies": { "@aws-sdk/client-s3": "3.933.0", + "@daytona/sdk": "0.167.0", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", @@ -864,6 +865,8 @@ "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.993.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.993.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-cognito-identity": "^3.972.3", "@aws-sdk/credential-provider-env": "^3.972.9", "@aws-sdk/credential-provider-http": "^3.972.11", "@aws-sdk/credential-provider-ini": "^3.972.9", "@aws-sdk/credential-provider-login": "^3.972.9", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/credential-provider-process": "^3.972.9", "@aws-sdk/credential-provider-sso": "^3.972.9", "@aws-sdk/credential-provider-web-identity": "^3.972.9", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-1M/nukgPSLqe9krzOKHnE8OylUaKAiokAV3xRLdeExVHcRE7WG5uzCTKWTj1imKvPjDqXq/FWhlbbdWIn7xIwA=="], + "@aws-sdk/lib-storage": ["@aws-sdk/lib-storage@3.1031.0", "", { "dependencies": { "@smithy/middleware-endpoint": "^4.4.30", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "buffer": "5.6.0", "events": "3.3.0", "stream-browserify": "3.0.0", "tslib": "^2.6.2" }, "peerDependencies": { "@aws-sdk/client-s3": "^3.1031.0" } }, "sha512-QC6MwdeXWZt9wnOV6/rgxSnVUcDeoHr5TCnCm0UPuSQOwuLX1/2QJa6oo7PdfdHg+i3G+VOnmG8QZ3tYxvBGRA=="], + "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@aws-sdk/util-arn-parser": "3.893.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-cnCLWeKPYgvV4yRYPFH6pWMdUByvu2cy2BAlfsPpvnm4RaVioztyvxmQj5PmVN5fvWs5w/2d6U7le8X9iye2sA=="], "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-5HEQ+JU4DrLNWeY27wKg/jeVa8Suy62ivJHOSUf6e6hZdVIMx0h/kXS1fHEQNNiLu2IzSEP/bFXsKBaW7x7s0g=="], @@ -1048,6 +1051,12 @@ "@ctrl/tinycolor": ["@ctrl/tinycolor@4.2.0", "", {}, "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A=="], + "@daytona/api-client": ["@daytona/api-client@0.167.0", "", { "dependencies": { "axios": "^1.6.1" } }, "sha512-2H1NhSu5GVVfg2oU9Mgt3VZRz7OUqACjBSb9GNNyC7uGPCnQh2C93Imqj2wfyUx/XF346hYpk+Nee+/NGqn3KA=="], + + "@daytona/sdk": ["@daytona/sdk@0.167.0", "", { "dependencies": { "@aws-sdk/client-s3": "^3.787.0", "@aws-sdk/lib-storage": "^3.798.0", "@daytona/api-client": "0.167.0", "@daytona/toolbox-api-client": "0.167.0", "@iarna/toml": "^2.2.5", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.207.0", "@opentelemetry/instrumentation-http": "^0.207.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-node": "^0.207.0", "@opentelemetry/sdk-trace-base": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0", "axios": "^1.13.5", "busboy": "^1.0.0", "dotenv": "^17.0.1", "expand-tilde": "^2.0.2", "fast-glob": "^3.3.0", "form-data": "^4.0.4", "isomorphic-ws": "^5.0.0", "pathe": "^2.0.3", "shell-quote": "^1.8.2", "tar": "^7.5.11" } }, "sha512-Xsb1bJ52tahpCiq2pEJdFVqqxLQoXO8laa2BFJFrSyLnGBMJXMaEormcgZfoOC9J6mVo9oOqTABLuoT+4DP11A=="], + + "@daytona/toolbox-api-client": ["@daytona/toolbox-api-client@0.167.0", "", { "dependencies": { "axios": "^1.6.1" } }, "sha512-FzSh6UVA8ptktNrRhIJCVfZMOcbXlVazTXej+YQ1rEj7L/PnGCQlBbT296xYj94C3wXQUTIxTZVkWzB4vcDTxw=="], + "@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="], "@dimforge/rapier2d-simd-compat": ["@dimforge/rapier2d-simd-compat@0.17.3", "", {}, "sha512-bijvwWz6NHsNj5e5i1vtd3dU2pDhthSaTUZSh14DUGGKJfw8eMnlWZsxwHBxB/a3AXVNDjL9abuHw1k9FGR+jg=="], @@ -1198,6 +1207,10 @@ "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="], + + "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="], "@hey-api/codegen-core": ["@hey-api/codegen-core@0.5.5", "", { "dependencies": { "@hey-api/types": "0.1.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-f2ZHucnA2wBGAY8ipB4wn/mrEYW+WUxU2huJmUvfDO6AE2vfILSHeF3wCO39Pz4wUYPoAWZByaauftLrOfC12Q=="], @@ -1216,6 +1229,8 @@ "@hono/zod-validator": ["@hono/zod-validator@0.4.2", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-1rrlBg+EpDPhzOV4hT9pxr5+xDVmKuz6YJl+la7VCwK6ass5ldyKm5fD+umJdV2zhHD6jROoCCv8NbTwyfhT0g=="], + "@iarna/toml": ["@iarna/toml@2.2.5", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="], + "@ibm/plex": ["@ibm/plex@6.4.1", "", { "dependencies": { "@ibm/telemetry-js": "^1.5.1" } }, "sha512-fnsipQywHt3zWvsnlyYKMikcVI7E2fEwpiPnIHFqlbByXVfQfANAAeJk1IV4mNnxhppUIDlhU0TzwYwL++Rn2g=="], "@ibm/telemetry-js": ["@ibm/telemetry-js@1.11.0", "", { "bin": { "ibmtelemetry": "dist/collect.js" } }, "sha512-RO/9j+URJnSfseWg9ZkEX9p+a3Ousd33DBU7rOafoZB08RqdzxFVYJ2/iM50dkBuD0o7WX7GYt1sLbNgCoE+pA=="], @@ -1346,6 +1361,8 @@ "@js-joda/core": ["@js-joda/core@5.7.0", "", {}, "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg=="], + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], @@ -1574,24 +1591,56 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="], + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ=="], "@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="], + "@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-grpc-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/sdk-logs": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-K92RN+kQGTMzFDsCzsYNGqOsXRUnko/Ckk+t/yPJao72MewOLgBUTWVHhebgkNfRCYqDz1v3K0aPT9OJkemvgg=="], + + "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/sdk-logs": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-JpOh7MguEUls8eRfkVVW3yRhClo5b9LqwWTOg8+i4gjr/+8eiCtquJnC7whvpTIGyff06cLZ2NsEj+CVP3Mjeg=="], + + "@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-RQJEV/K6KPbQrIUbsrRkEe0ufks1o5OGLHy6jbDD8tRjeCsbFHWfg99lYBRqBV33PYZJXsigqMaAbjWGTFYzLw=="], + + "@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-metrics-otlp-http": "0.207.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-grpc-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-6flX89W54gkwmqYShdcTBR1AEF5C1Ob0O8pDgmLPikTKyEv27lByr9yBmO5WrP0+5qJuNPHrLfgFQFYi6npDGA=="], + + "@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fG8FAJmvXOrKXGIRN8+y41U41IfVXxPRVwyB05LoMqYSjugx/FSBkMZUZXUT/wclTdmBKtS5MKoi0bEKkmRhSw=="], + + "@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-metrics-otlp-http": "0.207.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kDBxiTeQjaRlUQzS1COT9ic+et174toZH6jxaVuVAvGqmxOkgjpLOjrI5ff8SMMQE69r03L3Ll3nPKekLopLwg=="], + + "@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Y5p1s39FvIRmU+F1++j7ly8/KSqhMmn6cMfpQqiDCqDjdDHwUtSq0XI0WwL3HYGnZeaR/VV4BNmsYQJ7GAPrhw=="], + + "@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-grpc-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-7u2ZmcIx6D4KG/+5np4X2qA0o+O0K8cnUDhR4WI/vr5ZZ0la9J9RG+tkSjC7Yz+2XgL6760gSIM7/nyd3yaBLA=="], + "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw=="], - "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="], + "@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ruUQB4FkWtxHjNmSXjrhmJZFvyMm+tBzHyMm7YPQshApy4wvZUTcrpPyP/A/rCl/8M4BwoVIZdiwijMdbZaq4w=="], + + "@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-VV4QzhGCT7cWrGasBWxelBjqbNBbyHicWWS/66KoZoe9BzYwFB72SH2/kkc4uAviQlO8iwv2okIJy+/jqqEHTg=="], + + "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], + + "@opentelemetry/instrumentation-http": ["@opentelemetry/instrumentation-http@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/instrumentation": "0.207.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FC4i5hVixTzuhg4SV2ycTEAYx+0E2hm+GwbdoVPSA6kna0pPVI4etzaA9UkpJ9ussumQheFXP6rkGIaFJjMxsw=="], + + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eKFjKNdsPed4q9yYqeI5gBTLjXxDM/8jwhiC0icw3zKxHVGBySoDsed5J5q/PGY/3quzenTr3FiTxA3NiNT+nw=="], "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="], - "@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + "@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-9CrbTLFi5Ee4uepxg2qlpQIozoJuoAZU5sKMx0Mn7Oh+p7UrgCiEV6C02FOxxdYVRRFQVCinYR8Kf6eMSQsIsw=="], + + "@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FfeOHOrdhiNzecoB1jZKp2fybqmqMPJUXe2ZOydP7QzmTPYcfPeuaclTLYVhK3HyJf71kt8sTl92nV4YIaLaKA=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="], "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], + "@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.207.0", "@opentelemetry/exporter-logs-otlp-http": "0.207.0", "@opentelemetry/exporter-logs-otlp-proto": "0.207.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.207.0", "@opentelemetry/exporter-metrics-otlp-http": "0.207.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.207.0", "@opentelemetry/exporter-prometheus": "0.207.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.207.0", "@opentelemetry/exporter-trace-otlp-http": "0.207.0", "@opentelemetry/exporter-trace-otlp-proto": "0.207.0", "@opentelemetry/exporter-zipkin": "2.2.0", "@opentelemetry/instrumentation": "0.207.0", "@opentelemetry/propagator-b3": "2.2.0", "@opentelemetry/propagator-jaeger": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "@opentelemetry/sdk-trace-node": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-hnRsX/M8uj0WaXOBvFenQ8XsE8FLVh2uSnn1rkWu4mx+qu7EKGUZvZng6y/95cyzsqOfiaDDr08Ek4jppkIDNg=="], + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="], @@ -2506,6 +2555,8 @@ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], @@ -2728,6 +2779,8 @@ "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], "c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="], @@ -2788,6 +2841,8 @@ "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + "cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + "classnames": ["classnames@2.3.2", "", {}, "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="], "clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="], @@ -3000,7 +3055,7 @@ "dot-prop": ["dot-prop@8.0.2", "", { "dependencies": { "type-fest": "^3.8.0" } }, "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ=="], - "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], @@ -3152,6 +3207,8 @@ "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], + "expand-tilde": ["expand-tilde@2.0.2", "", { "dependencies": { "homedir-polyfill": "^1.0.1" } }, "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], @@ -3254,6 +3311,8 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + "forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="], + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], "framer-motion": ["framer-motion@8.5.5", "", { "dependencies": { "@motionone/dom": "^10.15.3", "hey-listen": "^1.0.8", "tslib": "^2.4.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-5IDx5bxkjWHWUF3CVJoSyUVOtrbAxtzYBBowRE2uYI/6VYhkEBD+rbTHEGuUmbGHRj6YqqSfoG7Aa1cLyWCrBA=="], @@ -3410,6 +3469,8 @@ "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="], + "homedir-polyfill": ["homedir-polyfill@1.0.3", "", { "dependencies": { "parse-passwd": "^1.0.0" } }, "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA=="], + "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], "hono-openapi": ["hono-openapi@1.1.2", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-toUcO60MftRBxqcVyxsHNYs2m4vf4xkQaiARAucQx3TiBPDtMNNkoh+C4I1vAretQZiGyaLOZNWn1YxfSyUA5g=="], @@ -3464,6 +3525,8 @@ "immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], + "import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], + "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="], "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], @@ -3726,6 +3789,8 @@ "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], @@ -3956,6 +4021,8 @@ "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], + "morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="], "motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="], @@ -4162,6 +4229,8 @@ "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="], + "parse-passwd": ["parse-passwd@1.0.0", "", {}, "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], @@ -4442,6 +4511,8 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="], @@ -4684,8 +4755,12 @@ "storybook-solidjs-vite": ["storybook-solidjs-vite@10.0.12", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.7.0", "@storybook/builder-vite": "^10.3.1", "@storybook/global": "^5.0.0", "vite-plugin-solid": "^2.11.11" }, "peerDependencies": { "solid-js": "^1.9.0", "storybook": "^0.0.0-0 || ^10.0.0", "typescript": "^4.0.0 || ^5.0.0 || ^6.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["typescript"] }, "sha512-KfKhJRdxbhFLHkBzLKSEk5sO2M/+KV9cdpki5Xdl5pwNP8kcoQnZ3b/okZk8dMRV6x19j86bKc7zDfc5bPSMwA=="], + "stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="], + "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], + "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], + "streamx": ["streamx@2.25.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="], "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -5100,7 +5175,7 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], @@ -5336,6 +5411,16 @@ "@aws-sdk/credential-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], + "@aws-sdk/lib-storage/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.30", "", { "dependencies": { "@smithy/core": "^3.23.15", "@smithy/middleware-serde": "^4.2.18", "@smithy/node-config-provider": "^4.3.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" } }, "sha512-qS2XqhKeXmdZ4nEQ4cOxIczSP/Y91wPAHYuRwmWDCh975B7/57uxsm5d6sisnUThn2u2FwzMdJNM7AbO1YPsPg=="], + + "@aws-sdk/lib-storage/@smithy/protocol-http": ["@smithy/protocol-http@5.3.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client": ["@smithy/smithy-client@4.12.11", "", { "dependencies": { "@smithy/core": "^3.23.15", "@smithy/middleware-endpoint": "^4.4.30", "@smithy/middleware-stack": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-stream": "^4.5.23", "tslib": "^2.6.2" } }, "sha512-wzz/Wa1CH/Tlhxh0s4DQPEcXSxSVfJ59AZcUh9Gu0c6JTlKuwGf4o/3P2TExv0VbtPFt8odIBG+eQGK2+vTECg=="], + + "@aws-sdk/lib-storage/@smithy/types": ["@smithy/types@4.14.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg=="], + + "@aws-sdk/lib-storage/buffer": ["buffer@5.6.0", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" } }, "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw=="], + "@aws-sdk/middleware-flexible-checksums/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], "@aws-sdk/middleware-sdk-s3/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], @@ -5398,6 +5483,8 @@ "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + "@daytona/sdk/@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HSRBzXHIC7C8UfPQdu15zEEoBGv0yWkhEwxqgPCHVUKUQ9NLHVGXkVrf65Uaj7UwmAkC1gQfkuVYvLlD//AnUQ=="], + "@develar/schema-utils/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -5442,6 +5529,8 @@ "@gitlab/opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "@grpc/proto-loader/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], "@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -5582,6 +5671,108 @@ "@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], + + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], + + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], + + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + + "@opentelemetry/exporter-prometheus/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/exporter-prometheus/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="], + + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/exporter-zipkin/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/exporter-zipkin/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/instrumentation-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/otlp-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/propagator-b3/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/propagator-jaeger/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/sdk-logs/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="], + + "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/sdk-node/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HSRBzXHIC7C8UfPQdu15zEEoBGv0yWkhEwxqgPCHVUKUQ9NLHVGXkVrf65Uaj7UwmAkC1gQfkuVYvLlD//AnUQ=="], + + "@opentelemetry/sdk-node/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], + + "@opentelemetry/sdk-node/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + + "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.2.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.2.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-+OaRja3f0IqGG2kptVeYsrZQK9nKRSpfFrKtRBq4uh6nIB8bTBgaGvYQrQoRrQWQMA5dK5yLhDMDc0dvYvCOIQ=="], + + "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + "@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], "@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="], @@ -5710,6 +5901,8 @@ "app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], + "app-builder-lib/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "app-builder-lib/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], "app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], @@ -5756,8 +5949,6 @@ "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], - "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], "compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], @@ -5788,6 +5979,8 @@ "dot-prop/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="], + "dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "editorconfig/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], "editorconfig/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -6022,6 +6215,8 @@ "storybook-solidjs-vite/vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="], + "stream-browserify/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -6030,8 +6225,6 @@ "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], @@ -6288,6 +6481,26 @@ "@aws-sdk/credential-providers/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core": ["@smithy/core@3.23.15", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-stream": "^4.5.23", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-E7GVCgsQttzfujEZb6Qep005wWf4xiL4x06apFEtzQMWYBPggZh/0cnOxPficw5cuK/YjjkehKoIN4YUaSh0UQ=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.18", "", { "dependencies": { "@smithy/core": "^3.23.15", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-M6CSgnp3v4tYz9ynj2JHbA60woBZcGqEwNjTKjBsNHPV26R1ZX52+0wW8WsZU18q45jD0tw2wL22S17Ze9LpEw=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.14", "", { "dependencies": { "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.9", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.14", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/util-middleware": ["@smithy/util-middleware@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core": ["@smithy/core@3.23.15", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-stream": "^4.5.23", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-E7GVCgsQttzfujEZb6Qep005wWf4xiL4x06apFEtzQMWYBPggZh/0cnOxPficw5cuK/YjjkehKoIN4YUaSh0UQ=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream": ["@smithy/util-stream@4.5.23", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.5.3", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-N6on1+ngJ3RznZOnDWNveIwnTSlqxNnXuNAh7ez889ZZaRdXoNRTXKgmYOLe6dB0gCmAVtuRScE1hymQFl4hpg=="], + + "@aws-sdk/lib-storage/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.17", "", { "dependencies": { "@smithy/types": "^4.14.0", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg=="], "@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-endpoints": "^3.3.4", "tslib": "^2.6.2" } }, "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg=="], @@ -6324,6 +6537,12 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@daytona/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@daytona/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@daytona/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + "@develar/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], @@ -6366,6 +6585,10 @@ "@gitlab/opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "@grpc/proto-loader/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "@grpc/proto-loader/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@jsx-email/cli/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], "@jsx-email/cli/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], @@ -6544,6 +6767,52 @@ "@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], + + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], + + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], + + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + + "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], + + "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + + "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], + + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.2.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ=="], + "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], @@ -6668,6 +6937,10 @@ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "motion/framer-motion/motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="], "motion/framer-motion/motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="], @@ -6690,6 +6963,8 @@ "opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "openid-client/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "ora/bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "ora/bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -6850,10 +7125,34 @@ "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.23", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.5.3", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-N6on1+ngJ3RznZOnDWNveIwnTSlqxNnXuNAh7ez889ZZaRdXoNRTXKgmYOLe6dB0gCmAVtuRScE1hymQFl4hpg=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/url-parser": ["@smithy/url-parser@4.2.14", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.17", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.3", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-lc5jFL++x17sPhIwMWJ3YOnqmSjw/2Po6VLDlUIXvxVWRuJwRXnJ4jOBBLB0cfI5BB5ehIl02Fxr1PDvk/kxDw=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + "@daytona/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], + + "@daytona/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + "@electron/asar/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "@electron/rebuild/node-gyp/make-fetch-happen/@npmcli/agent": ["@npmcli/agent@3.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q=="], @@ -6878,6 +7177,14 @@ "@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@grpc/proto-loader/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@grpc/proto-loader/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "@grpc/proto-loader/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@grpc/proto-loader/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@jsx-email/cli/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@jsx-email/cli/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -6964,6 +7271,8 @@ "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "app-builder-lib/hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "archiver-utils/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], "archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], @@ -7080,6 +7389,16 @@ "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.17", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.3", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-lc5jFL++x17sPhIwMWJ3YOnqmSjw/2Po6VLDlUIXvxVWRuJwRXnJ4jOBBLB0cfI5BB5ehIl02Fxr1PDvk/kxDw=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A=="], + + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A=="], + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], @@ -7098,6 +7417,10 @@ "@electron/rebuild/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@grpc/proto-loader/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@grpc/proto-loader/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="], @@ -7136,6 +7459,10 @@ "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A=="], + + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A=="], + "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -7156,6 +7483,8 @@ "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch/minipass-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], diff --git a/daytona.ts b/daytona.ts new file mode 100644 index 0000000000..0315560eae --- /dev/null +++ b/daytona.ts @@ -0,0 +1,199 @@ +import type { Daytona, Sandbox } from "@daytonaio/sdk" +import type { Plugin } from "@opencode-ai/plugin" +import { join } from "node:path" +import { fileURLToPath } from "node:url" +import { tmpdir } from "node:os" +import { access, copyFile, mkdir } from "node:fs/promises" + +let client: Promise | undefined + +let daytona = function daytona(): Promise { + if (client == null) { + client = import("@daytonaio/sdk").then( + ({ Daytona }) => + new Daytona({ + apiKey: "dtn_d63c206564ef49d4104ec2cd755e561bb3665beed8fd7d7ab2c5f7a2186965f0", + }), + ) + } + return client +} + + + +const preview = new Map() +const repo = "/home/daytona/workspace/repo" +const root = "/home/daytona/workspace" +const localbin = "/home/daytona/opencode" +const installbin = "/home/daytona/.opencode/bin/opencode" +const health = "http://127.0.0.1:3096/global/health" + +const local = fileURLToPath( + new URL("./packages/opencode/dist/opencode-linux-x64-baseline/bin/opencode", import.meta.url), +) + +async function exists(file: string) { + return access(file) + .then(() => true) + .catch(() => false) +} + +function sh(value: string) { + return `'${value.replace(/'/g, `"'"'"`)}'` +} + +// Internally Daytona uses axios, which tries to overwrite stack +// traces when a failure happens. That path fails in Bun, however, so +// when something goes wrong you only see a very obscure error. +async function withSandbox(name: string, fn: (sandbox: Sandbox) => Promise) { + const stack = Error.captureStackTrace + // @ts-expect-error temporary compatibility hack for Daytona's axios stack handling in Bun + Error.captureStackTrace = undefined + try { + return await fn(await (await daytona()).get(name)) + } finally { + Error.captureStackTrace = stack + } +} + +export const DaytonaWorkspacePlugin: Plugin = async ({ experimental_workspace, worktree, project }) => { + experimental_workspace.register("daytona", { + name: "Daytona", + description: "Create a remote Daytona workspace", + configure(config) { + return config + }, + async create(config, env) { + const temp = join(tmpdir(), `opencode-daytona-${Date.now()}`) + + console.log("creating sandbox...") + + const sandbox = await ( + await daytona() + ).create({ + name: config.name, + snapshot: "daytona-large", + envVars: env, + }) + + console.log("creating ssh...") + + const ssh = await withSandbox(config.name, (sandbox) => sandbox.createSshAccess()) + console.log("daytona:", ssh.sshCommand) + + const run = async (command: string) => { + console.log("sandbox:", command) + const result = await sandbox.process.executeCommand(command) + if (result.result) process.stdout.write(result.result) + if (result.exitCode === 0) return result + throw new Error(result.result || `sandbox command failed: ${command}`) + } + + const wait = async () => { + for (let i = 0; i < 60; i++) { + const result = await sandbox.process.executeCommand(`curl -fsS ${sh(health)}`) + if (result.exitCode === 0) { + if (result.result) process.stdout.write(result.result) + return + } + console.log(`waiting for server (${i + 1}/60)`) + await Bun.sleep(1000) + } + + const log = await sandbox.process.executeCommand(`test -f /tmp/opencode.log && cat /tmp/opencode.log || true`) + throw new Error(log.result || "daytona workspace server did not become ready in time") + } + + const dir = join(temp, "repo") + const tar = join(temp, "repo.tgz") + const source = `file://${worktree}` + await mkdir(temp, { recursive: true }) + const args = ["clone", "--depth", "1", "--no-local"] + if (config.branch) args.push("--branch", config.branch) + args.push(source, dir) + + console.log("git cloning...") + + const clone = Bun.spawn(["git", ...args], { + cwd: tmpdir(), + stdout: "pipe", + stderr: "pipe", + }) + const code = await clone.exited + if (code !== 0) throw new Error(await new Response(clone.stderr).text()) + + const configPackage = join(worktree, ".opencode", "package.json") + // if (await exists(configPackage)) { + // console.log("copying config package...") + // await mkdir(join(dir, ".opencode"), { recursive: true }) + // await copyFile(configPackage, join(dir, ".opencode", "package.json")) + // } + + console.log("tarring...") + + const packed = Bun.spawn(["tar", "-czf", tar, "-C", temp, "repo"], { + stdout: "ignore", + stderr: "pipe", + }) + if ((await packed.exited) !== 0) throw new Error(await new Response(packed.stderr).text()) + + console.log("uploading files...") + + await sandbox.fs.uploadFile(tar, "repo.tgz") + + const have = await exists(local) + console.log("local", local) + if (have) { + console.log("uploading local binary...") + await sandbox.fs.uploadFile(local, "opencode") + } + + console.log("bootstrapping workspace...") + await run(`rm -rf ${sh(repo)} && mkdir -p ${sh(root)} && tar -xzf \"$HOME/repo.tgz\" -C \"$HOME/workspace\"`) + + if (have) { + await run(`chmod +x ${sh(localbin)}`) + } else { + await run( + `mkdir -p \"$HOME/.opencode/bin\" && OPENCODE_INSTALL_DIR=\"$HOME/.opencode/bin\" curl -fsSL https://opencode.ai/install | bash`, + ) + } + + await run(`printf \"%s\\n\" ${sh(project.id)} > ${sh(`${repo}/.git/opencode`)}`) + + console.log("starting server...") + await run( + `cd ${sh(repo)} && exe=${sh(localbin)} && if [ ! -x \"$exe\" ]; then exe=${sh(installbin)}; fi && nohup env \"$exe\" serve --hostname 0.0.0.0 --port 3096 >/tmp/opencode.log 2>&1 undefined) + if (!sandbox) return + await (await daytona()).delete(sandbox) + preview.delete(config.name) + }, + async target(config) { + let link = preview.get(config.name) + if (!link) { + link = await withSandbox(config.name, (sandbox) => sandbox.getPreviewLink(3096)) + preview.set(config.name, link) + } + return { + type: "remote", + url: link.url, + headers: { + "x-daytona-preview-token": link.token, + "x-daytona-skip-preview-warning": "true", + "x-opencode-directory": repo, + }, + } + }, + }) + + return {} +} + +export default DaytonaWorkspacePlugin diff --git a/package.json b/package.json index 5fecc09922..09548bf4e0 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "3.933.0", + "@daytona/sdk": "0.167.0", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", diff --git a/packages/opencode/src/cli/cmd/tui/context/project.tsx b/packages/opencode/src/cli/cmd/tui/context/project.tsx index 26e5c075d7..22dd94bc82 100644 --- a/packages/opencode/src/cli/cmd/tui/context/project.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/project.tsx @@ -10,18 +10,21 @@ export const { use: useProject, provider: ProjectProvider } = createSimpleContex name: "Project", init: () => { const sdk = useSDK() + + const defaultPath = { + home: "", + state: "", + config: "", + worktree: "", + directory: sdk.directory ?? "", + } satisfies Path + const [store, setStore] = createStore({ project: { id: undefined as string | undefined, }, instance: { - path: { - home: "", - state: "", - config: "", - worktree: "", - directory: sdk.directory ?? "", - } satisfies Path, + path: defaultPath, }, workspace: { current: undefined as string | undefined, @@ -38,7 +41,7 @@ export const { use: useProject, provider: ProjectProvider } = createSimpleContex ]) batch(() => { - setStore("instance", "path", reconcile(path.data!)) + setStore("instance", "path", reconcile(path.data || defaultPath)) setStore("project", "id", project.data?.id) }) } diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index b5734e67d0..57326e3a1a 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -27,7 +27,7 @@ import { createSimpleContext } from "./helper" import type { Snapshot } from "@/snapshot" import { useExit } from "./exit" import { useArgs } from "./args" -import { batch, createEffect, on } from "solid-js" +import { batch, onMount } from "solid-js" import { Log } from "@/util" import { emptyConsoleState, type ConsoleState } from "@/config/console-state" @@ -108,6 +108,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const project = useProject() const sdk = useSDK() + const fullSyncedSessions = new Set() + let syncedWorkspace = project.workspace.current() + event.subscribe((event) => { switch (event.type) { case "server.instance.disposed": @@ -350,9 +353,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const exit = useExit() const args = useArgs() - async function bootstrap() { - console.log("bootstrapping") + async function bootstrap(input: { fatal?: boolean } = {}) { + const fatal = input.fatal ?? true const workspace = project.workspace.current() + if (workspace !== syncedWorkspace) { + fullSyncedSessions.clear() + syncedWorkspace = workspace + } const start = Date.now() - 30 * 24 * 60 * 60 * 1000 const sessionListPromise = sdk.client.session .list({ start: start }) @@ -441,20 +448,17 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ name: e instanceof Error ? e.name : undefined, stack: e instanceof Error ? e.stack : undefined, }) - await exit(e) + if (fatal) { + await exit(e) + } else { + throw e + } }) } - const fullSyncedSessions = new Set() - createEffect( - on( - () => project.workspace.current(), - () => { - fullSyncedSessions.clear() - void bootstrap() - }, - ), - ) + onMount(() => { + void bootstrap() + }) const result = { data: store, diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 7c40e6c3c0..70a4b73b95 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -181,23 +181,30 @@ export function Session() { const sdk = useSDK() createEffect(async () => { - await sdk.client.session - .get({ sessionID: route.sessionID }, { throwOnError: true }) - .then((x) => { - project.workspace.set(x.data?.workspaceID) - }) - .then(() => sync.session.sync(route.sessionID)) - .then(() => { - if (scroll) scroll.scrollBy(100_000) - }) - .catch((e) => { - console.error(e) - toast.show({ - message: `Session not found: ${route.sessionID}`, - variant: "error", - }) - return navigate({ type: "home" }) + const previousWorkspace = project.workspace.current() + const result = await sdk.client.session.get({ sessionID: route.sessionID }, { throwOnError: true }) + if (!result.data) { + toast.show({ + message: `Session not found: ${route.sessionID}`, + variant: "error", }) + navigate({ type: "home" }) + return + } + + if (result.data.workspaceID !== previousWorkspace) { + project.workspace.set(result.data.workspaceID) + + // Sync all the data for this workspace. Note that this + // workspace may not exist anymore which is why this is not + // fatal. If it doesn't we still want to show the session + // (which will be non-interactive) + try { + await sync.bootstrap({ fatal: false }) + } catch (e) {} + } + await sync.session.sync(route.sessionID) + if (scroll) scroll.scrollBy(100_000) }) // Handle initial prompt from fork diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index a453b19815..077cc43097 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -6,7 +6,6 @@ import { Decimal } from "decimal.js" import z from "zod" import { type ProviderMetadata, type LanguageModelUsage } from "ai" import { Flag } from "../flag/flag" -import { Installation } from "../installation" import { InstallationVersion } from "../installation/version" import { Database, NotFoundError, eq, and, gte, isNull, desc, like, inArray, lt } from "../storage" @@ -713,8 +712,10 @@ export function* list(input?: { if (input?.workspaceID) { conditions.push(eq(SessionTable.workspace_id, input.workspaceID)) } - if (input?.directory) { - conditions.push(eq(SessionTable.directory, input.directory)) + if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) { + if (input?.directory) { + conditions.push(eq(SessionTable.directory, input.directory)) + } } if (input?.roots) { conditions.push(isNull(SessionTable.parent_id)) diff --git a/packages/opencode/test/cli/tui/sync-provider.test.tsx b/packages/opencode/test/cli/tui/sync-provider.test.tsx deleted file mode 100644 index e75e186199..0000000000 --- a/packages/opencode/test/cli/tui/sync-provider.test.tsx +++ /dev/null @@ -1,280 +0,0 @@ -/** @jsxImportSource @opentui/solid */ -import { afterEach, describe, expect, test } from "bun:test" -import { testRender } from "@opentui/solid" -import { onMount } from "solid-js" -import { ArgsProvider } from "../../../src/cli/cmd/tui/context/args" -import { ExitProvider } from "../../../src/cli/cmd/tui/context/exit" -import { ProjectProvider, useProject } from "../../../src/cli/cmd/tui/context/project" -import { SDKProvider } from "../../../src/cli/cmd/tui/context/sdk" -import { SyncProvider, useSync } from "../../../src/cli/cmd/tui/context/sync" - -const sighup = new Set(process.listeners("SIGHUP")) - -afterEach(() => { - for (const fn of process.listeners("SIGHUP")) { - if (!sighup.has(fn)) process.off("SIGHUP", fn) - } -}) - -function json(data: unknown) { - return new Response(JSON.stringify(data), { - headers: { - "content-type": "application/json", - }, - }) -} - -async function wait(fn: () => boolean, timeout = 2000) { - const start = Date.now() - while (!fn()) { - if (Date.now() - start > timeout) throw new Error("timed out waiting for condition") - await Bun.sleep(10) - } -} - -function data(workspace?: string | null) { - const tag = workspace ?? "root" - return { - session: { - id: "ses_1", - title: `session-${tag}`, - workspaceID: workspace ?? undefined, - time: { - updated: 1, - }, - }, - message: { - info: { - id: "msg_1", - sessionID: "ses_1", - role: "assistant", - time: { - created: 1, - completed: 1, - }, - }, - parts: [ - { - id: "part_1", - messageID: "msg_1", - sessionID: "ses_1", - type: "text", - text: `part-${tag}`, - }, - ], - }, - todo: [ - { - id: `todo-${tag}`, - content: `todo-${tag}`, - status: "pending", - priority: "medium", - }, - ], - diff: [ - { - file: `${tag}.ts`, - patch: "", - additions: 0, - deletions: 0, - }, - ], - } -} - -type Hit = { - path: string - workspace?: string -} - -function createFetch(log: Hit[]) { - return Object.assign( - async (input: RequestInfo | URL, init?: RequestInit) => { - const req = new Request(input, init) - const url = new URL(req.url) - const workspace = url.searchParams.get("workspace") ?? req.headers.get("x-opencode-workspace") ?? undefined - log.push({ - path: url.pathname, - workspace, - }) - - if (url.pathname === "/config/providers") { - return json({ providers: [], default: {} }) - } - if (url.pathname === "/provider") { - return json({ all: [], default: {}, connected: [] }) - } - if (url.pathname === "/experimental/console") { - return json({}) - } - if (url.pathname === "/agent") { - return json([]) - } - if (url.pathname === "/config") { - return json({}) - } - if (url.pathname === "/project/current") { - return json({ id: `proj-${workspace ?? "root"}` }) - } - if (url.pathname === "/path") { - return json({ - state: `/tmp/${workspace ?? "root"}/state`, - config: `/tmp/${workspace ?? "root"}/config`, - worktree: "/tmp/worktree", - directory: `/tmp/${workspace ?? "root"}`, - }) - } - if (url.pathname === "/session") { - return json([]) - } - if (url.pathname === "/command") { - return json([]) - } - if (url.pathname === "/lsp") { - return json([]) - } - if (url.pathname === "/mcp") { - return json({}) - } - if (url.pathname === "/experimental/resource") { - return json({}) - } - if (url.pathname === "/formatter") { - return json([]) - } - if (url.pathname === "/session/status") { - return json({}) - } - if (url.pathname === "/provider/auth") { - return json({}) - } - if (url.pathname === "/vcs") { - return json({ branch: "main" }) - } - if (url.pathname === "/experimental/workspace") { - return json([{ id: "ws_a" }, { id: "ws_b" }]) - } - if (url.pathname === "/session/ses_1") { - return json(data(workspace).session) - } - if (url.pathname === "/session/ses_1/message") { - return json([data(workspace).message]) - } - if (url.pathname === "/session/ses_1/todo") { - return json(data(workspace).todo) - } - if (url.pathname === "/session/ses_1/diff") { - return json(data(workspace).diff) - } - - throw new Error(`unexpected request: ${req.method} ${url.pathname}`) - }, - { preconnect: fetch.preconnect.bind(fetch) }, - ) satisfies typeof fetch -} - -async function mount(log: Hit[]) { - let project!: ReturnType - let sync!: ReturnType - let done!: () => void - const ready = new Promise((resolve) => { - done = resolve - }) - - const app = await testRender(() => ( - () => {} }} - > - - - - - { - project = ctx.project - sync = ctx.sync - done() - }} - /> - - - - - - )) - - await ready - return { app, project, sync } -} - -async function waitBoot(log: Hit[], workspace?: string) { - await wait(() => log.some((item) => item.path === "/experimental/workspace")) - if (!workspace) return - await wait(() => log.some((item) => item.path === "/project/current" && item.workspace === workspace)) -} - -function Probe(props: { - onReady: (ctx: { project: ReturnType; sync: ReturnType }) => void -}) { - const project = useProject() - const sync = useSync() - - onMount(() => { - props.onReady({ project, sync }) - }) - - return -} - -describe("SyncProvider", () => { - test("re-runs bootstrap requests when the active workspace changes", async () => { - const log: Hit[] = [] - const { app, project } = await mount(log) - - try { - await waitBoot(log) - log.length = 0 - - project.workspace.set("ws_a") - - await waitBoot(log, "ws_a") - - expect(log.some((item) => item.path === "/path" && item.workspace === "ws_a")).toBe(true) - expect(log.some((item) => item.path === "/config" && item.workspace === "ws_a")).toBe(true) - expect(log.some((item) => item.path === "/command" && item.workspace === "ws_a")).toBe(true) - } finally { - app.renderer.destroy() - } - }) - - test("clears full-sync cache when the active workspace changes", async () => { - const log: Hit[] = [] - const { app, project, sync } = await mount(log) - - try { - await waitBoot(log) - - log.length = 0 - project.workspace.set("ws_a") - await waitBoot(log, "ws_a") - expect(project.workspace.current()).toBe("ws_a") - - log.length = 0 - await sync.session.sync("ses_1") - expect(log.filter((item) => item.path === "/session/ses_1")).toHaveLength(1) - - project.workspace.set("ws_b") - await waitBoot(log, "ws_b") - expect(project.workspace.current()).toBe("ws_b") - - log.length = 0 - await sync.session.sync("ses_1") - expect(log.filter((item) => item.path === "/session/ses_1")).toHaveLength(1) - } finally { - app.renderer.destroy() - } - }) -}) From 4260c40efa332deeebaf730382d5388adc95d024 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 23:36:21 -0400 Subject: [PATCH 064/335] refactor(tui): inline final Go shimmer settings (#23017) --- .../src/cli/cmd/tui/component/logo.tsx | 49 ++++++++++++++++++- .../cli/cmd/tui/component/shimmer-config.ts | 49 ------------------- 2 files changed, 48 insertions(+), 50 deletions(-) delete mode 100644 packages/opencode/src/cli/cmd/tui/component/shimmer-config.ts diff --git a/packages/opencode/src/cli/cmd/tui/component/logo.tsx b/packages/opencode/src/cli/cmd/tui/component/logo.tsx index 17368ddad8..bee104a35d 100644 --- a/packages/opencode/src/cli/cmd/tui/component/logo.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/logo.tsx @@ -3,13 +3,60 @@ import { For, createMemo, createSignal, onCleanup, onMount, type JSX } from "sol import { useTheme, tint } from "@tui/context/theme" import * as Sound from "@tui/util/sound" import { go, logo } from "@/cli/logo" -import { shimmerConfig, type ShimmerConfig } from "./shimmer-config" export type LogoShape = { left: string[] right: string[] } +type ShimmerConfig = { + period: number + rings: number + sweepFraction: number + coreWidth: number + coreAmp: number + softWidth: number + softAmp: number + tail: number + tailAmp: number + haloWidth: number + haloOffset: number + haloAmp: number + breathBase: number + noise: number + ambientAmp: number + ambientCenter: number + ambientWidth: number + shadowMix: number + primaryMix: number + originX: number + originY: number +} + +const shimmerConfig: ShimmerConfig = { + period: 4600, + rings: 2, + sweepFraction: 1, + coreWidth: 1.2, + coreAmp: 1.9, + softWidth: 10, + softAmp: 1.6, + tail: 5, + tailAmp: 0.64, + haloWidth: 4.3, + haloOffset: 0.6, + haloAmp: 0.16, + breathBase: 0.04, + noise: 0.1, + ambientAmp: 0.36, + ambientCenter: 0.5, + ambientWidth: 0.34, + shadowMix: 0.1, + primaryMix: 0.3, + originX: 4.5, + originY: 13.5, +} + // Shadow markers (rendered chars in parens): // _ = full shadow cell (space with bg=shadow) // ^ = letter top, shadow bottom (▀ with fg=letter, bg=shadow) diff --git a/packages/opencode/src/cli/cmd/tui/component/shimmer-config.ts b/packages/opencode/src/cli/cmd/tui/component/shimmer-config.ts deleted file mode 100644 index 01bc136f5d..0000000000 --- a/packages/opencode/src/cli/cmd/tui/component/shimmer-config.ts +++ /dev/null @@ -1,49 +0,0 @@ -export type ShimmerConfig = { - period: number - rings: number - sweepFraction: number - coreWidth: number - coreAmp: number - softWidth: number - softAmp: number - tail: number - tailAmp: number - haloWidth: number - haloOffset: number - haloAmp: number - breathBase: number - noise: number - ambientAmp: number - ambientCenter: number - ambientWidth: number - shadowMix: number - primaryMix: number - originX: number - originY: number -} - -export const shimmerDefaults: ShimmerConfig = { - period: 4600, - rings: 2, - sweepFraction: 1, - coreWidth: 1.2, - coreAmp: 1.9, - softWidth: 10, - softAmp: 1.6, - tail: 5, - tailAmp: 0.64, - haloWidth: 4.3, - haloOffset: 0.6, - haloAmp: 0.16, - breathBase: 0.04, - noise: 0.1, - ambientAmp: 0.36, - ambientCenter: 0.5, - ambientWidth: 0.34, - shadowMix: 0.1, - primaryMix: 0.3, - originX: 4.5, - originY: 13.5, -} - -export const shimmerConfig: ShimmerConfig = { ...shimmerDefaults } From 67dbb3cf18279c1a8f3f60e242d9b09e1270d01a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 03:37:21 +0000 Subject: [PATCH 065/335] chore: generate --- daytona.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/daytona.ts b/daytona.ts index 0315560eae..4007b77aa6 100644 --- a/daytona.ts +++ b/daytona.ts @@ -19,8 +19,6 @@ let daytona = function daytona(): Promise { return client } - - const preview = new Map() const repo = "/home/daytona/workspace/repo" const root = "/home/daytona/workspace" From 9ee89f7868bbdb5b412b9b475c41af22ac4f2a20 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 16 Apr 2026 23:48:12 -0400 Subject: [PATCH 066/335] refactor: move project read routes onto HttpApi (#23003) --- packages/opencode/src/project/project.ts | 76 ++++++++++--------- .../src/server/instance/httpapi/project.ts | 62 +++++++++++++++ .../src/server/instance/httpapi/server.ts | 3 + .../opencode/src/server/instance/index.ts | 17 +++-- .../opencode/src/server/instance/project.ts | 8 +- 5 files changed, 119 insertions(+), 47 deletions(-) create mode 100644 packages/opencode/src/server/instance/httpapi/project.ts diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index f838d9ab43..6a2132274a 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -8,46 +8,52 @@ import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" import { which } from "../util/which" import { ProjectID } from "./schema" -import { Effect, Layer, Path, Scope, Context, Stream } from "effect" +import { Effect, Layer, Path, Scope, Context, Stream, Types, Schema } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NodePath } from "@effect/platform-node" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" const log = Log.create({ service: "project" }) -export const Info = z - .object({ - id: ProjectID.zod, - worktree: z.string(), - vcs: z.literal("git").optional(), - name: z.string().optional(), - icon: z - .object({ - url: z.string().optional(), - override: z.string().optional(), - color: z.string().optional(), - }) - .optional(), - commands: z - .object({ - start: z.string().optional().describe("Startup script to run when creating a new workspace (worktree)"), - }) - .optional(), - time: z.object({ - created: z.number(), - updated: z.number(), - initialized: z.number().optional(), - }), - sandboxes: z.array(z.string()), - }) - .meta({ - ref: "Project", - }) -export type Info = z.infer +const ProjectVcs = Schema.Literal("git") + +const ProjectIcon = Schema.Struct({ + url: Schema.optional(Schema.String), + override: Schema.optional(Schema.String), + color: Schema.optional(Schema.String), +}) + +const ProjectCommands = Schema.Struct({ + start: Schema.optional( + Schema.String.annotate({ description: "Startup script to run when creating a new workspace (worktree)" }), + ), +}) + +const ProjectTime = Schema.Struct({ + created: Schema.Number, + updated: Schema.Number, + initialized: Schema.optional(Schema.Number), +}) + +export const Info = Schema.Struct({ + id: ProjectID, + worktree: Schema.String, + vcs: Schema.optional(ProjectVcs), + name: Schema.optional(Schema.String), + icon: Schema.optional(ProjectIcon), + commands: Schema.optional(ProjectCommands), + time: ProjectTime, + sandboxes: Schema.Array(Schema.String), +}) + .annotate({ identifier: "Project" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Info = Types.DeepMutable> export const Event = { - Updated: BusEvent.define("project.updated", Info), + Updated: BusEvent.define("project.updated", Info.zod), } type Row = typeof ProjectTable.$inferSelect @@ -58,7 +64,7 @@ export function fromRow(row: Row): Info { return { id: row.id, worktree: row.worktree, - vcs: row.vcs ? Info.shape.vcs.parse(row.vcs) : undefined, + vcs: row.vcs ? Schema.decodeUnknownSync(ProjectVcs)(row.vcs) : undefined, name: row.name ?? undefined, icon, time: { @@ -74,8 +80,8 @@ export function fromRow(row: Row): Info { export const UpdateInput = z.object({ projectID: ProjectID.zod, name: z.string().optional(), - icon: Info.shape.icon.optional(), - commands: Info.shape.commands.optional(), + icon: zod(ProjectIcon).optional(), + commands: zod(ProjectCommands).optional(), }) export type UpdateInput = z.infer @@ -139,7 +145,7 @@ export const layer: Layer.Layer< }), ) - const fakeVcs = Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS) + const fakeVcs = Schema.decodeUnknownSync(Schema.optional(ProjectVcs))(Flag.OPENCODE_FAKE_VCS) const resolveGitPath = (cwd: string, name: string) => { if (!name) return cwd diff --git a/packages/opencode/src/server/instance/httpapi/project.ts b/packages/opencode/src/server/instance/httpapi/project.ts new file mode 100644 index 0000000000..7d2d8462f0 --- /dev/null +++ b/packages/opencode/src/server/instance/httpapi/project.ts @@ -0,0 +1,62 @@ +import { Instance } from "@/project/instance" +import { Project } from "@/project" +import { Effect, Layer, Schema } from "effect" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" + +const root = "/project" + +export const ProjectApi = HttpApi.make("project") + .add( + HttpApiGroup.make("project") + .add( + HttpApiEndpoint.get("list", root, { + success: Schema.Array(Project.Info), + }).annotateMerge( + OpenApi.annotations({ + identifier: "project.list", + summary: "List all projects", + description: "Get a list of projects that have been opened with OpenCode.", + }), + ), + HttpApiEndpoint.get("current", `${root}/current`, { + success: Project.Info, + }).annotateMerge( + OpenApi.annotations({ + identifier: "project.current", + summary: "Get current project", + description: "Retrieve the currently active project that OpenCode is working with.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "project", + description: "Experimental HttpApi project routes.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "opencode experimental HttpApi", + version: "0.0.1", + description: "Experimental HttpApi surface for selected instance routes.", + }), + ) + +export const projectHandlers = Layer.unwrap( + Effect.gen(function* () { + const svc = yield* Project.Service + + const list = Effect.fn("ProjectHttpApi.list")(function* () { + return yield* svc.list() + }) + + const current = Effect.fn("ProjectHttpApi.current")(function* () { + return Instance.project + }) + + return HttpApiBuilder.group(ProjectApi, "project", (handlers) => + handlers.handle("list", list).handle("current", current), + ) + }), +).pipe(Layer.provide(Project.defaultLayer)) diff --git a/packages/opencode/src/server/instance/httpapi/server.ts b/packages/opencode/src/server/instance/httpapi/server.ts index 64332fd2a0..b4442d6400 100644 --- a/packages/opencode/src/server/instance/httpapi/server.ts +++ b/packages/opencode/src/server/instance/httpapi/server.ts @@ -12,6 +12,7 @@ import { lazy } from "@/util/lazy" import { Filesystem } from "@/util" import { ConfigApi, configHandlers } from "./config" import { PermissionApi, permissionHandlers } from "./permission" +import { ProjectApi, projectHandlers } from "./project" import { ProviderApi, providerHandlers } from "./provider" import { QuestionApi, questionHandlers } from "./question" @@ -108,11 +109,13 @@ const instance = HttpRouter.middleware()( const QuestionSecured = QuestionApi.middleware(Authorization) const PermissionSecured = PermissionApi.middleware(Authorization) +const ProjectSecured = ProjectApi.middleware(Authorization) const ProviderSecured = ProviderApi.middleware(Authorization) const ConfigSecured = ConfigApi.middleware(Authorization) export const routes = Layer.mergeAll( HttpApiBuilder.layer(ConfigSecured).pipe(Layer.provide(configHandlers)), + HttpApiBuilder.layer(ProjectSecured).pipe(Layer.provide(projectHandlers)), HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)), HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)), HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)), diff --git a/packages/opencode/src/server/instance/index.ts b/packages/opencode/src/server/instance/index.ts index 6a290093c5..cfcaffc596 100644 --- a/packages/opencode/src/server/instance/index.ts +++ b/packages/opencode/src/server/instance/index.ts @@ -30,14 +30,7 @@ import { WorkspaceRouterMiddleware } from "./middleware" import { AppRuntime } from "@/effect/app-runtime" export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { - const app = new Hono() - .use(WorkspaceRouterMiddleware(upgrade)) - .route("/project", ProjectRoutes()) - .route("/pty", PtyRoutes(upgrade)) - .route("/config", ConfigRoutes()) - .route("/experimental", ExperimentalRoutes()) - .route("/session", SessionRoutes()) - .route("/permission", PermissionRoutes()) + const app = new Hono().use(WorkspaceRouterMiddleware(upgrade)) if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) { const handler = ExperimentalHttpApiServer.webHandler().handler @@ -52,9 +45,17 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { app.get("/provider/auth", (c) => handler(c.req.raw, context)) app.post("/provider/:providerID/oauth/authorize", (c) => handler(c.req.raw, context)) app.post("/provider/:providerID/oauth/callback", (c) => handler(c.req.raw, context)) + app.get("/project", (c) => handler(c.req.raw, context)) + app.get("/project/current", (c) => handler(c.req.raw, context)) } return app + .route("/project", ProjectRoutes()) + .route("/pty", PtyRoutes(upgrade)) + .route("/config", ConfigRoutes()) + .route("/experimental", ExperimentalRoutes()) + .route("/session", SessionRoutes()) + .route("/permission", PermissionRoutes()) .route("/question", QuestionRoutes()) .route("/provider", ProviderRoutes()) .route("/sync", SyncRoutes()) diff --git a/packages/opencode/src/server/instance/project.ts b/packages/opencode/src/server/instance/project.ts index eea741596d..95b5862fd5 100644 --- a/packages/opencode/src/server/instance/project.ts +++ b/packages/opencode/src/server/instance/project.ts @@ -23,7 +23,7 @@ export const ProjectRoutes = lazy(() => description: "List of projects", content: { "application/json": { - schema: resolver(Project.Info.array()), + schema: resolver(Project.Info.zod.array()), }, }, }, @@ -45,7 +45,7 @@ export const ProjectRoutes = lazy(() => description: "Current project information", content: { "application/json": { - schema: resolver(Project.Info), + schema: resolver(Project.Info.zod), }, }, }, @@ -66,7 +66,7 @@ export const ProjectRoutes = lazy(() => description: "Project information after git initialization", content: { "application/json": { - schema: resolver(Project.Info), + schema: resolver(Project.Info.zod), }, }, }, @@ -99,7 +99,7 @@ export const ProjectRoutes = lazy(() => description: "Updated project information", content: { "application/json": { - schema: resolver(Project.Info), + schema: resolver(Project.Info.zod), }, }, }, From 79e9baf55acd6b5d2c8b49486cbd6a507897bf84 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:54:19 +0800 Subject: [PATCH 067/335] fix(app): use fetchQuery instead of ensureQueryData in global sync (#23025) --- packages/app/src/context/global-sync.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 6ff60f161f..1359b07b4e 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -204,7 +204,7 @@ function createGlobalSync() { const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT) const promise = queryClient - .ensureQueryData({ + .fetchQuery({ ...loadSessionsQuery(directory), queryFn: () => loadRootSessionsWithFallback({ From dfaae1454454f02db7879ad7bdfd8a18feba83b5 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 04:14:26 +0000 Subject: [PATCH 068/335] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 239b72fd70..54fd991eca 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-tYAb5Mo39UW1VEejYuo0jW0jzH2OyY/HrqgiZL3rmjY=", - "aarch64-linux": "sha256-3zGKV5UwokXpmY0nT1mry3IhNf2EQYLKT7ac+/trmQA=", - "aarch64-darwin": "sha256-oKXAut7eu/eW5a43OT8+aFuH1F1tuIldTs+7PUXSCv4=", - "x86_64-darwin": "sha256-Az+9X1scOEhw3aOO8laKJoZjiuz3qlLTIk1bx25P/z4=" + "x86_64-linux": "sha256-OPbZUo/fQv2Xsf+NEZV08GLBMN/DXovhRvn2JkesFtY=", + "aarch64-linux": "sha256-WK7xlVLuirKDN5LaqjBn7qpv5bYVtYHZw0qRNKX4xXg=", + "aarch64-darwin": "sha256-BAoAdeLQ+lXDD7Klxoxij683OVVug8KXEMRUqIQAjc8=", + "x86_64-darwin": "sha256-ZOBwNR2gZgc5f+y3VIBBT4qZpeZfg7Of6AaGDOfqsG8=" } } From 4bd5a158a5cbc09ac52df8dc7001fb3dc4110506 Mon Sep 17 00:00:00 2001 From: Dax Date: Fri, 17 Apr 2026 00:23:30 -0400 Subject: [PATCH 069/335] fix: preserve prompt input across unmount/remount cycles (#22508) --- packages/opencode/src/cli/cmd/tui/app.tsx | 4 --- .../cli/cmd/tui/component/prompt/index.tsx | 19 +++++++++++- .../src/cli/cmd/tui/context/route.tsx | 8 ++--- .../opencode/src/cli/cmd/tui/plugin/api.tsx | 2 +- .../opencode/src/cli/cmd/tui/routes/home.tsx | 5 ++-- .../session/dialog-fork-from-timeline.tsx | 4 +-- .../cmd/tui/routes/session/dialog-message.tsx | 30 +++++++++---------- .../src/cli/cmd/tui/routes/session/index.tsx | 7 ++--- packages/plugin/src/tui.ts | 2 +- 9 files changed, 45 insertions(+), 36 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 8255c007d0..7e883ec0e3 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -420,12 +420,8 @@ function App(props: { onSnapshot?: () => Promise }) { aliases: ["clear"], }, onSelect: () => { - const current = promptRef.current - // Don't require focus - if there's any text, preserve it - const currentPrompt = current?.current?.input ? current.current : undefined route.navigate({ type: "home", - initialPrompt: currentPrompt, }) dialog.clear() }, diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 82c4a7222f..82cdefebcb 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -12,7 +12,7 @@ import { useRoute } from "@tui/context/route" import { useSync } from "@tui/context/sync" import { useEvent } from "@tui/context/event" import { MessageID, PartID } from "@/session/schema" -import { createStore, produce } from "solid-js/store" +import { createStore, produce, unwrap } from "solid-js/store" import { useKeybind } from "@tui/context/keybind" import { usePromptHistory, type PromptInfo } from "./history" import { assign } from "./part" @@ -75,6 +75,8 @@ function randomIndex(count: number) { return Math.floor(Math.random() * count) } +let stashed: { prompt: PromptInfo; cursor: number } | undefined + export function Prompt(props: PromptProps) { let input: TextareaRenderable let anchor: BoxRenderable @@ -433,7 +435,22 @@ export function Prompt(props: PromptProps) { }, } + onMount(() => { + const saved = stashed + stashed = undefined + if (store.prompt.input) return + if (saved && saved.prompt.input) { + input.setText(saved.prompt.input) + setStore("prompt", saved.prompt) + restoreExtmarksFromParts(saved.prompt.parts) + input.cursorOffset = saved.cursor + } + }) + onCleanup(() => { + if (store.prompt.input) { + stashed = { prompt: unwrap(store.prompt), cursor: input.cursorOffset } + } props.ref?.(undefined) }) diff --git a/packages/opencode/src/cli/cmd/tui/context/route.tsx b/packages/opencode/src/cli/cmd/tui/context/route.tsx index e9f463a13f..6db8247592 100644 --- a/packages/opencode/src/cli/cmd/tui/context/route.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/route.tsx @@ -1,16 +1,16 @@ -import { createStore } from "solid-js/store" +import { createStore, reconcile } from "solid-js/store" import { createSimpleContext } from "./helper" import type { PromptInfo } from "../component/prompt/history" export type HomeRoute = { type: "home" - initialPrompt?: PromptInfo + prompt?: PromptInfo } export type SessionRoute = { type: "session" sessionID: string - initialPrompt?: PromptInfo + prompt?: PromptInfo } export type PluginRoute = { @@ -37,7 +37,7 @@ export const { use: useRoute, provider: RouteProvider } = createSimpleContext({ return store }, navigate(route: Route) { - setStore(route) + setStore(reconcile(route)) }, } }, diff --git a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx index d2b495ca31..5bea483807 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx +++ b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx @@ -91,7 +91,7 @@ function routeCurrent(route: ReturnType): TuiPluginApi["route"] name: "session", params: { sessionID: route.data.sessionID, - initialPrompt: route.data.initialPrompt, + prompt: route.data.prompt, }, } } diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index 1cce7fb396..2f0ff07e9a 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -10,7 +10,6 @@ import { usePromptRef } from "../context/prompt" import { useLocal } from "../context/local" import { TuiPluginRuntime } from "../plugin" -// TODO: what is the best way to do this? let once = false const placeholder = { normal: ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"], @@ -31,8 +30,8 @@ export function Home() { setRef(r) promptRef.set(r) if (once || !r) return - if (route.initialPrompt) { - r.set(route.initialPrompt) + if (route.prompt) { + r.set(route.prompt) once = true return } diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx index 0ce33a59a9..8d1e4438c8 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx @@ -38,7 +38,7 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess messageID: message.id, }) const parts = sync.data.part[message.id] ?? [] - const initialPrompt = parts.reduce( + const prompt = parts.reduce( (agg, part) => { if (part.type === "text") { if (!part.synthetic) agg.input += part.text @@ -51,7 +51,7 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess route.navigate({ sessionID: forked.data!.id, type: "session", - initialPrompt, + prompt, }) dialog.clear() }, diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx index 412b4d87eb..aeea2f52ad 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx @@ -81,25 +81,23 @@ export function DialogMessage(props: { sessionID: props.sessionID, messageID: props.messageID, }) - const initialPrompt = (() => { - const msg = message() - if (!msg) return undefined - const parts = sync.data.part[msg.id] - return parts.reduce( - (agg, part) => { - if (part.type === "text") { - if (!part.synthetic) agg.input += part.text - } - if (part.type === "file") agg.parts.push(part) - return agg - }, - { input: "", parts: [] as PromptInfo["parts"] }, - ) - })() + const msg = message() + const prompt = msg + ? sync.data.part[msg.id].reduce( + (agg, part) => { + if (part.type === "text") { + if (!part.synthetic) agg.input += part.text + } + if (part.type === "file") agg.parts.push(part) + return agg + }, + { input: "", parts: [] as PromptInfo["parts"] }, + ) + : undefined route.navigate({ sessionID: result.data!.id, type: "session", - initialPrompt, + prompt, }) dialog.clear() }, diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 70a4b73b95..ccca4d1eba 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -207,8 +207,6 @@ export function Session() { if (scroll) scroll.scrollBy(100_000) }) - // Handle initial prompt from fork - let seeded = false let lastSwitch: string | undefined = undefined event.on("message.part.updated", (evt) => { const part = evt.properties.part @@ -226,14 +224,15 @@ export function Session() { } }) + let seeded = false let scroll: ScrollBoxRenderable let prompt: PromptRef | undefined const bind = (r: PromptRef | undefined) => { prompt = r promptRef.set(r) - if (seeded || !route.initialPrompt || !r) return + if (seeded || !route.prompt || !r) return seeded = true - r.set(route.initialPrompt) + r.set(route.prompt) } const keybind = useKeybind() const dialog = useDialog() diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index 099cf27580..1c57a71ab3 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -29,7 +29,7 @@ export type TuiRouteCurrent = name: "session" params: { sessionID: string - initialPrompt?: unknown + prompt?: unknown } } | { From 76a141090ea33da574d06e17cd1c5dbddbde2952 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 16 Apr 2026 23:31:21 -0500 Subject: [PATCH 070/335] chore: delete filetime module (#22999) --- .opencode/agent/translator.md | 1 - packages/opencode/specs/effect/migration.md | 4 +- packages/opencode/src/effect/app-runtime.ts | 2 - packages/opencode/src/file/time.ts | 113 ----- packages/opencode/src/flag/flag.ts | 1 - packages/opencode/src/session/prompt.ts | 4 - packages/opencode/src/tool/edit.ts | 119 +++-- packages/opencode/src/tool/read.ts | 7 +- packages/opencode/src/tool/registry.ts | 3 - packages/opencode/src/tool/write.ts | 4 - packages/opencode/test/file/time.test.ts | 422 ------------------ .../test/session/prompt-effect.test.ts | 12 - .../test/session/snapshot-tool-race.test.ts | 12 - packages/opencode/test/tool/edit.test.ts | 88 ---- packages/opencode/test/tool/read.test.ts | 2 - packages/opencode/test/tool/write.test.ts | 13 - packages/web/src/content/docs/ar/cli.mdx | 1 - packages/web/src/content/docs/bs/cli.mdx | 1 - packages/web/src/content/docs/cli.mdx | 1 - packages/web/src/content/docs/da/cli.mdx | 1 - packages/web/src/content/docs/de/cli.mdx | 1 - packages/web/src/content/docs/es/cli.mdx | 1 - packages/web/src/content/docs/fr/cli.mdx | 1 - packages/web/src/content/docs/it/cli.mdx | 1 - packages/web/src/content/docs/ja/cli.mdx | 1 - packages/web/src/content/docs/ko/cli.mdx | 1 - packages/web/src/content/docs/nb/cli.mdx | 1 - packages/web/src/content/docs/pl/cli.mdx | 1 - packages/web/src/content/docs/pt-br/cli.mdx | 1 - packages/web/src/content/docs/ru/cli.mdx | 1 - packages/web/src/content/docs/th/cli.mdx | 1 - packages/web/src/content/docs/tr/cli.mdx | 1 - packages/web/src/content/docs/zh-cn/cli.mdx | 1 - packages/web/src/content/docs/zh-tw/cli.mdx | 1 - 34 files changed, 59 insertions(+), 766 deletions(-) delete mode 100644 packages/opencode/src/file/time.ts delete mode 100644 packages/opencode/test/file/time.test.ts diff --git a/.opencode/agent/translator.md b/.opencode/agent/translator.md index a987d01927..8ac7025f17 100644 --- a/.opencode/agent/translator.md +++ b/.opencode/agent/translator.md @@ -594,7 +594,6 @@ OPENCODE_DISABLE_CLAUDE_CODE OPENCODE_DISABLE_CLAUDE_CODE_PROMPT OPENCODE_DISABLE_CLAUDE_CODE_SKILLS OPENCODE_DISABLE_DEFAULT_PLUGINS -OPENCODE_DISABLE_FILETIME_CHECK OPENCODE_DISABLE_LSP_DOWNLOAD OPENCODE_DISABLE_MODELS_FETCH OPENCODE_DISABLE_PRUNE diff --git a/packages/opencode/specs/effect/migration.md b/packages/opencode/specs/effect/migration.md index 105a82290b..b8bf4e0494 100644 --- a/packages/opencode/specs/effect/migration.md +++ b/packages/opencode/specs/effect/migration.md @@ -9,7 +9,7 @@ Use `InstanceState` (from `src/effect/instance-state.ts`) for services that need Use `makeRuntime` (from `src/effect/run-service.ts`) to create a per-service `ManagedRuntime` that lazily initializes and shares layers via a global `memoMap`. Returns `{ runPromise, runFork, runCallback }`. - Global services (no per-directory state): Account, Auth, AppFileSystem, Installation, Truncate, Worktree -- Instance-scoped (per-directory state via InstanceState): Agent, Bus, Command, Config, File, FileTime, FileWatcher, Format, LSP, MCP, Permission, Plugin, ProviderAuth, Pty, Question, SessionStatus, Skill, Snapshot, ToolRegistry, Vcs +- Instance-scoped (per-directory state via InstanceState): Agent, Bus, Command, Config, File, FileWatcher, Format, LSP, MCP, Permission, Plugin, ProviderAuth, Pty, Question, SessionStatus, Skill, Snapshot, ToolRegistry, Vcs Rule of thumb: if two open directories should not share one copy of the service, it needs `InstanceState`. @@ -195,7 +195,6 @@ This checklist is only about the service shape migration. Many of these services - [x] `Config` — `config/config.ts` - [x] `Discovery` — `skill/discovery.ts` (dependency-only layer, no standalone runtime) - [x] `File` — `file/index.ts` -- [x] `FileTime` — `file/time.ts` - [x] `FileWatcher` — `file/watcher.ts` - [x] `Format` — `format/index.ts` - [x] `Installation` — `installation/index.ts` @@ -301,7 +300,6 @@ For each service, the migration is roughly: - `SessionRunState` — migrated 2026-04-11. Single caller in `server/instance/session.ts` converted; facade removed. - `Account` — migrated 2026-04-11. Callers in `server/instance/experimental.ts` and `cli/cmd/account.ts` converted; facade removed. - `Instruction` — migrated 2026-04-11. Test-only callers converted; facade removed. -- `FileTime` — migrated 2026-04-11. Test-only callers converted; facade removed. - `FileWatcher` — migrated 2026-04-11. Callers in `project/bootstrap.ts` and test converted; facade removed. - `Question` — migrated 2026-04-11. Callers in `server/instance/question.ts` and test converted; facade removed. - `Truncate` — migrated 2026-04-11. Caller in `tool/tool.ts` and test converted; facade removed. diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index a9ed957745..eae52d6366 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -9,7 +9,6 @@ import { Account } from "@/account/account" import { Config } from "@/config" import { Git } from "@/git" import { Ripgrep } from "@/file/ripgrep" -import { FileTime } from "@/file/time" import { File } from "@/file" import { FileWatcher } from "@/file/watcher" import { Storage } from "@/storage" @@ -58,7 +57,6 @@ export const AppLayer = Layer.mergeAll( Config.defaultLayer, Git.defaultLayer, Ripgrep.defaultLayer, - FileTime.defaultLayer, File.defaultLayer, FileWatcher.defaultLayer, Storage.defaultLayer, diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts deleted file mode 100644 index cc26682d57..0000000000 --- a/packages/opencode/src/file/time.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { DateTime, Effect, Layer, Option, Semaphore, Context } from "effect" -import { InstanceState } from "@/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Flag } from "@/flag/flag" -import type { SessionID } from "@/session/schema" -import { Log } from "../util" - -const log = Log.create({ service: "file.time" }) - -export type Stamp = { - readonly read: Date - readonly mtime: number | undefined - readonly size: number | undefined -} - -const session = (reads: Map>, sessionID: SessionID) => { - const value = reads.get(sessionID) - if (value) return value - - const next = new Map() - reads.set(sessionID, next) - return next -} - -interface State { - reads: Map> - locks: Map -} - -export interface Interface { - readonly read: (sessionID: SessionID, file: string) => Effect.Effect - readonly get: (sessionID: SessionID, file: string) => Effect.Effect - readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect - readonly withLock: (filepath: string, fn: () => Effect.Effect) => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/FileTime") {} - -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const fsys = yield* AppFileSystem.Service - const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK - - const stamp = Effect.fnUntraced(function* (file: string) { - const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.void)) - return { - read: yield* DateTime.nowAsDate, - mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined, - size: info ? Number(info.size) : undefined, - } - }) - const state = yield* InstanceState.make( - Effect.fn("FileTime.state")(() => - Effect.succeed({ - reads: new Map>(), - locks: new Map(), - }), - ), - ) - - const getLock = Effect.fn("FileTime.lock")(function* (filepath: string) { - filepath = AppFileSystem.normalizePath(filepath) - const locks = (yield* InstanceState.get(state)).locks - const lock = locks.get(filepath) - if (lock) return lock - - const next = Semaphore.makeUnsafe(1) - locks.set(filepath, next) - return next - }) - - const read = Effect.fn("FileTime.read")(function* (sessionID: SessionID, file: string) { - file = AppFileSystem.normalizePath(file) - const reads = (yield* InstanceState.get(state)).reads - log.info("read", { sessionID, file }) - session(reads, sessionID).set(file, yield* stamp(file)) - }) - - const get = Effect.fn("FileTime.get")(function* (sessionID: SessionID, file: string) { - file = AppFileSystem.normalizePath(file) - const reads = (yield* InstanceState.get(state)).reads - return reads.get(sessionID)?.get(file)?.read - }) - - const assert = Effect.fn("FileTime.assert")(function* (sessionID: SessionID, filepath: string) { - if (disableCheck) return - filepath = AppFileSystem.normalizePath(filepath) - - const reads = (yield* InstanceState.get(state)).reads - const time = reads.get(sessionID)?.get(filepath) - if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`) - - const next = yield* stamp(filepath) - const changed = next.mtime !== time.mtime || next.size !== time.size - if (!changed) return - - throw new Error( - `File ${filepath} has been modified since it was last read.\nLast modification: ${new Date(next.mtime ?? next.read.getTime()).toISOString()}\nLast read: ${time.read.toISOString()}\n\nPlease read the file again before modifying it.`, - ) - }) - - const withLock = Effect.fn("FileTime.withLock")(function* (filepath: string, fn: () => Effect.Effect) { - return yield* fn().pipe((yield* getLock(filepath)).withPermits(1)) - }) - - return Service.of({ read, get, assert, withLock }) - }), -).pipe(Layer.orDie) - -export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) - -export * as FileTime from "./time" diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 416f641c44..a1c46288a2 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -70,7 +70,6 @@ export const Flag = { OPENCODE_EXPERIMENTAL_OXFMT: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_OXFMT"), OPENCODE_EXPERIMENTAL_LSP_TY: truthy("OPENCODE_EXPERIMENTAL_LSP_TY"), OPENCODE_EXPERIMENTAL_LSP_TOOL: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL"), - OPENCODE_DISABLE_FILETIME_CHECK: Config.boolean("OPENCODE_DISABLE_FILETIME_CHECK").pipe(Config.withDefault(false)), OPENCODE_EXPERIMENTAL_PLAN_MODE: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE"), OPENCODE_EXPERIMENTAL_MARKDOWN: !falsy("OPENCODE_EXPERIMENTAL_MARKDOWN"), OPENCODE_MODELS_URL: process.env["OPENCODE_MODELS_URL"], diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 9faa618788..431189d19c 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -23,7 +23,6 @@ import MAX_STEPS from "../session/prompt/max-steps.txt" import { ToolRegistry } from "../tool" import { MCP } from "../mcp" import { LSP } from "../lsp" -import { FileTime } from "../file/time" import { Flag } from "../flag/flag" import { ulid } from "ulid" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" @@ -94,7 +93,6 @@ export const layer = Layer.effect( const fsys = yield* AppFileSystem.Service const mcp = yield* MCP.Service const lsp = yield* LSP.Service - const filetime = yield* FileTime.Service const registry = yield* ToolRegistry.Service const truncate = yield* Truncate.Service const spawner = yield* ChildProcessSpawner.ChildProcessSpawner @@ -1183,7 +1181,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the ] } - yield* filetime.read(input.sessionID, filepath) return [ { messageID: info.id, @@ -1684,7 +1681,6 @@ export const defaultLayer = Layer.suspend(() => Layer.provide(Permission.defaultLayer), Layer.provide(MCP.defaultLayer), Layer.provide(LSP.defaultLayer), - Layer.provide(FileTime.defaultLayer), Layer.provide(ToolRegistry.defaultLayer), Layer.provide(Truncate.defaultLayer), Layer.provide(Provider.defaultLayer), diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 62b96cba82..f535183d4c 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -14,7 +14,6 @@ import { File } from "../file" import { FileWatcher } from "../file/watcher" import { Bus } from "../bus" import { Format } from "../format" -import { FileTime } from "../file/time" import { Instance } from "../project/instance" import { Snapshot } from "@/snapshot" import { assertExternalDirectoryEffect } from "./external-directory" @@ -44,7 +43,6 @@ export const EditTool = Tool.define( "edit", Effect.gen(function* () { const lsp = yield* LSP.Service - const filetime = yield* FileTime.Service const afs = yield* AppFileSystem.Service const format = yield* Format.Service const bus = yield* Bus.Service @@ -70,52 +68,11 @@ export const EditTool = Tool.define( let diff = "" let contentOld = "" let contentNew = "" - yield* filetime.withLock(filePath, () => - Effect.gen(function* () { - if (params.oldString === "") { - const existed = yield* afs.existsSafe(filePath) - contentNew = params.newString - diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew)) - yield* ctx.ask({ - permission: "edit", - patterns: [path.relative(Instance.worktree, filePath)], - always: ["*"], - metadata: { - filepath: filePath, - diff, - }, - }) - yield* afs.writeWithDirs(filePath, params.newString) - yield* format.file(filePath) - yield* bus.publish(File.Event.Edited, { file: filePath }) - yield* bus.publish(FileWatcher.Event.Updated, { - file: filePath, - event: existed ? "change" : "add", - }) - yield* filetime.read(ctx.sessionID, filePath) - return - } - - const info = yield* afs.stat(filePath).pipe(Effect.catch(() => Effect.succeed(undefined))) - if (!info) throw new Error(`File ${filePath} not found`) - if (info.type === "Directory") throw new Error(`Path is a directory, not a file: ${filePath}`) - yield* filetime.assert(ctx.sessionID, filePath) - contentOld = yield* afs.readFileString(filePath) - - const ending = detectLineEnding(contentOld) - const old = convertToLineEnding(normalizeLineEndings(params.oldString), ending) - const next = convertToLineEnding(normalizeLineEndings(params.newString), ending) - - contentNew = replace(contentOld, old, next, params.replaceAll) - - diff = trimDiff( - createTwoFilesPatch( - filePath, - filePath, - normalizeLineEndings(contentOld), - normalizeLineEndings(contentNew), - ), - ) + yield* Effect.gen(function* () { + if (params.oldString === "") { + const existed = yield* afs.existsSafe(filePath) + contentNew = params.newString + diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew)) yield* ctx.ask({ permission: "edit", patterns: [path.relative(Instance.worktree, filePath)], @@ -125,26 +82,62 @@ export const EditTool = Tool.define( diff, }, }) - - yield* afs.writeWithDirs(filePath, contentNew) + yield* afs.writeWithDirs(filePath, params.newString) yield* format.file(filePath) yield* bus.publish(File.Event.Edited, { file: filePath }) yield* bus.publish(FileWatcher.Event.Updated, { file: filePath, - event: "change", + event: existed ? "change" : "add", }) - contentNew = yield* afs.readFileString(filePath) - diff = trimDiff( - createTwoFilesPatch( - filePath, - filePath, - normalizeLineEndings(contentOld), - normalizeLineEndings(contentNew), - ), - ) - yield* filetime.read(ctx.sessionID, filePath) - }).pipe(Effect.orDie), - ) + return + } + + const info = yield* afs.stat(filePath).pipe(Effect.catch(() => Effect.succeed(undefined))) + if (!info) throw new Error(`File ${filePath} not found`) + if (info.type === "Directory") throw new Error(`Path is a directory, not a file: ${filePath}`) + contentOld = yield* afs.readFileString(filePath) + + const ending = detectLineEnding(contentOld) + const old = convertToLineEnding(normalizeLineEndings(params.oldString), ending) + const next = convertToLineEnding(normalizeLineEndings(params.newString), ending) + + contentNew = replace(contentOld, old, next, params.replaceAll) + + diff = trimDiff( + createTwoFilesPatch( + filePath, + filePath, + normalizeLineEndings(contentOld), + normalizeLineEndings(contentNew), + ), + ) + yield* ctx.ask({ + permission: "edit", + patterns: [path.relative(Instance.worktree, filePath)], + always: ["*"], + metadata: { + filepath: filePath, + diff, + }, + }) + + yield* afs.writeWithDirs(filePath, contentNew) + yield* format.file(filePath) + yield* bus.publish(File.Event.Edited, { file: filePath }) + yield* bus.publish(FileWatcher.Event.Updated, { + file: filePath, + event: "change", + }) + contentNew = yield* afs.readFileString(filePath) + diff = trimDiff( + createTwoFilesPatch( + filePath, + filePath, + normalizeLineEndings(contentOld), + normalizeLineEndings(contentNew), + ), + ) + }).pipe(Effect.orDie) const filediff: Snapshot.FileDiff = { file: filePath, diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index c6d1461cdf..18c668ca07 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -7,7 +7,6 @@ import { createInterface } from "readline" import * as Tool from "./tool" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { LSP } from "../lsp" -import { FileTime } from "../file/time" import DESCRIPTION from "./read.txt" import { Instance } from "../project/instance" import { assertExternalDirectoryEffect } from "./external-directory" @@ -31,7 +30,6 @@ export const ReadTool = Tool.define( const fs = yield* AppFileSystem.Service const instruction = yield* Instruction.Service const lsp = yield* LSP.Service - const time = yield* FileTime.Service const scope = yield* Scope.Scope const miss = Effect.fn("ReadTool.miss")(function* (filepath: string) { @@ -75,9 +73,8 @@ export const ReadTool = Tool.define( ).pipe(Effect.map((items: string[]) => items.sort((a, b) => a.localeCompare(b)))) }) - const warm = Effect.fn("ReadTool.warm")(function* (filepath: string, sessionID: Tool.Context["sessionID"]) { + const warm = Effect.fn("ReadTool.warm")(function* (filepath: string) { yield* lsp.touchFile(filepath, false).pipe(Effect.ignore, Effect.forkIn(scope)) - yield* time.read(sessionID, filepath) }) const run = Effect.fn("ReadTool.execute")(function* (params: z.infer, ctx: Tool.Context) { @@ -196,7 +193,7 @@ export const ReadTool = Tool.define( } output += "\n" - yield* warm(filepath, ctx.sessionID) + yield* warm(filepath) if (loaded.length > 0) { output += `\n\n\n${loaded.map((item) => item.content).join("\n\n")}\n` diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 34e6b4e364..e27593e597 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -39,7 +39,6 @@ import { InstanceState } from "@/effect" import { Question } from "../question" import { Todo } from "../session/todo" import { LSP } from "../lsp" -import { FileTime } from "../file/time" import { Instruction } from "../session/instruction" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Bus } from "../bus" @@ -80,7 +79,6 @@ export const layer: Layer.Layer< | Session.Service | Provider.Service | LSP.Service - | FileTime.Service | Instruction.Service | AppFileSystem.Service | Bus.Service @@ -329,7 +327,6 @@ export const defaultLayer = Layer.suspend(() => Layer.provide(Session.defaultLayer), Layer.provide(Provider.defaultLayer), Layer.provide(LSP.defaultLayer), - Layer.provide(FileTime.defaultLayer), Layer.provide(Instruction.defaultLayer), Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Bus.layer), diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index c5871eb0ef..741091b21d 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -9,7 +9,6 @@ import { Bus } from "../bus" import { File } from "../file" import { FileWatcher } from "../file/watcher" import { Format } from "../format" -import { FileTime } from "../file/time" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Instance } from "../project/instance" import { trimDiff } from "./edit" @@ -22,7 +21,6 @@ export const WriteTool = Tool.define( Effect.gen(function* () { const lsp = yield* LSP.Service const fs = yield* AppFileSystem.Service - const filetime = yield* FileTime.Service const bus = yield* Bus.Service const format = yield* Format.Service @@ -41,7 +39,6 @@ export const WriteTool = Tool.define( const exists = yield* fs.existsSafe(filepath) const contentOld = exists ? yield* fs.readFileString(filepath) : "" - if (exists) yield* filetime.assert(ctx.sessionID, filepath) const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, params.content)) yield* ctx.ask({ @@ -61,7 +58,6 @@ export const WriteTool = Tool.define( file: filepath, event: exists ? "change" : "add", }) - yield* filetime.read(ctx.sessionID, filepath) let output = "Wrote file successfully." yield* lsp.touchFile(filepath, true) diff --git a/packages/opencode/test/file/time.test.ts b/packages/opencode/test/file/time.test.ts deleted file mode 100644 index cb6390df87..0000000000 --- a/packages/opencode/test/file/time.test.ts +++ /dev/null @@ -1,422 +0,0 @@ -import { afterEach, describe, expect } from "bun:test" -import fs from "fs/promises" -import path from "path" -import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" -import { FileTime } from "../../src/file/time" -import { Instance } from "../../src/project/instance" -import { SessionID } from "../../src/session/schema" -import { Filesystem } from "../../src/util" -import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture" -import { testEffect } from "../lib/effect" - -afterEach(async () => { - await Instance.disposeAll() -}) - -const it = testEffect(Layer.mergeAll(FileTime.defaultLayer, CrossSpawnSpawner.defaultLayer)) - -const id = SessionID.make("ses_00000000000000000000000001") - -const put = (file: string, text: string) => Effect.promise(() => fs.writeFile(file, text, "utf-8")) - -const touch = (file: string, time: number) => - Effect.promise(() => { - const date = new Date(time) - return fs.utimes(file, date, date) - }) - -const read = (id: SessionID, file: string) => FileTime.Service.use((svc) => svc.read(id, file)) - -const get = (id: SessionID, file: string) => FileTime.Service.use((svc) => svc.get(id, file)) - -const check = (id: SessionID, file: string) => FileTime.Service.use((svc) => svc.assert(id, file)) - -const lock =
(file: string, fn: () => Effect.Effect) => FileTime.Service.use((svc) => svc.withLock(file, fn)) - -const fail = Effect.fn("FileTimeTest.fail")(function* (self: Effect.Effect) { - const exit = yield* self.pipe(Effect.exit) - if (Exit.isFailure(exit)) { - const err = Cause.squash(exit.cause) - return err instanceof Error ? err : new Error(String(err)) - } - throw new Error("expected file time effect to fail") -}) - -describe("file/time", () => { - describe("read() and get()", () => { - it.live("stores read timestamp", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const file = path.join(dir, "file.txt") - yield* put(file, "content") - - const before = yield* get(id, file) - expect(before).toBeUndefined() - - yield* read(id, file) - - const after = yield* get(id, file) - expect(after).toBeInstanceOf(Date) - expect(after!.getTime()).toBeGreaterThan(0) - }), - ), - ) - - it.live("tracks separate timestamps per session", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const file = path.join(dir, "file.txt") - yield* put(file, "content") - - const one = SessionID.make("ses_00000000000000000000000002") - const two = SessionID.make("ses_00000000000000000000000003") - yield* read(one, file) - yield* read(two, file) - - const first = yield* get(one, file) - const second = yield* get(two, file) - - expect(first).toBeDefined() - expect(second).toBeDefined() - }), - ), - ) - - it.live("updates timestamp on subsequent reads", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const file = path.join(dir, "file.txt") - yield* put(file, "content") - - yield* read(id, file) - const first = yield* get(id, file) - - yield* read(id, file) - const second = yield* get(id, file) - - expect(second!.getTime()).toBeGreaterThanOrEqual(first!.getTime()) - }), - ), - ) - - it.live("isolates reads by directory", () => - Effect.gen(function* () { - const one = yield* tmpdirScoped() - const two = yield* tmpdirScoped() - const shared = yield* tmpdirScoped() - const file = path.join(shared, "file.txt") - yield* put(file, "content") - - yield* provideInstance(one)(read(id, file)) - const result = yield* provideInstance(two)(get(id, file)) - expect(result).toBeUndefined() - }), - ) - }) - - describe("assert()", () => { - it.live("passes when file has not been modified", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const file = path.join(dir, "file.txt") - yield* put(file, "content") - yield* touch(file, 1_000) - - yield* read(id, file) - yield* check(id, file) - }), - ), - ) - - it.live("throws when file was not read first", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const file = path.join(dir, "file.txt") - yield* put(file, "content") - - const err = yield* fail(check(id, file)) - expect(err.message).toContain("You must read file") - }), - ), - ) - - it.live("throws when file was modified after read", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const file = path.join(dir, "file.txt") - yield* put(file, "content") - yield* touch(file, 1_000) - - yield* read(id, file) - yield* put(file, "modified content") - yield* touch(file, 2_000) - - const err = yield* fail(check(id, file)) - expect(err.message).toContain("modified since it was last read") - }), - ), - ) - - it.live("includes timestamps in error message", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const file = path.join(dir, "file.txt") - yield* put(file, "content") - yield* touch(file, 1_000) - - yield* read(id, file) - yield* put(file, "modified") - yield* touch(file, 2_000) - - const err = yield* fail(check(id, file)) - expect(err.message).toContain("Last modification:") - expect(err.message).toContain("Last read:") - }), - ), - ) - }) - - describe("withLock()", () => { - it.live("executes function within lock", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const file = path.join(dir, "file.txt") - let hit = false - - yield* lock(file, () => - Effect.sync(() => { - hit = true - return "result" - }), - ) - - expect(hit).toBe(true) - }), - ), - ) - - it.live("returns function result", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const file = path.join(dir, "file.txt") - const result = yield* lock(file, () => Effect.succeed("success")) - expect(result).toBe("success") - }), - ), - ) - - it.live("serializes concurrent operations on same file", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const file = path.join(dir, "file.txt") - const order: number[] = [] - const hold = yield* Deferred.make() - const ready = yield* Deferred.make() - - const one = yield* lock(file, () => - Effect.gen(function* () { - order.push(1) - yield* Deferred.succeed(ready, void 0) - yield* Deferred.await(hold) - order.push(2) - }), - ).pipe(Effect.forkScoped) - - yield* Deferred.await(ready) - - const two = yield* lock(file, () => - Effect.sync(() => { - order.push(3) - order.push(4) - }), - ).pipe(Effect.forkScoped) - - yield* Deferred.succeed(hold, void 0) - yield* Fiber.join(one) - yield* Fiber.join(two) - - expect(order).toEqual([1, 2, 3, 4]) - }), - ), - ) - - it.live("allows concurrent operations on different files", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const onefile = path.join(dir, "file1.txt") - const twofile = path.join(dir, "file2.txt") - let one = false - let two = false - const hold = yield* Deferred.make() - const ready = yield* Deferred.make() - - const a = yield* lock(onefile, () => - Effect.gen(function* () { - one = true - yield* Deferred.succeed(ready, void 0) - yield* Deferred.await(hold) - expect(two).toBe(true) - }), - ).pipe(Effect.forkScoped) - - yield* Deferred.await(ready) - - const b = yield* lock(twofile, () => - Effect.sync(() => { - two = true - }), - ).pipe(Effect.forkScoped) - - yield* Fiber.join(b) - yield* Deferred.succeed(hold, void 0) - yield* Fiber.join(a) - - expect(one).toBe(true) - expect(two).toBe(true) - }), - ), - ) - - it.live("releases lock even if function throws", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const file = path.join(dir, "file.txt") - const err = yield* fail(lock(file, () => Effect.die(new Error("Test error")))) - expect(err.message).toContain("Test error") - - let hit = false - yield* lock(file, () => - Effect.sync(() => { - hit = true - }), - ) - expect(hit).toBe(true) - }), - ), - ) - }) - - describe("path normalization", () => { - it.live("read with forward slashes, assert with backslashes", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const file = path.join(dir, "file.txt") - yield* put(file, "content") - yield* touch(file, 1_000) - - const forward = file.replaceAll("\\", "/") - yield* read(id, forward) - yield* check(id, file) - }), - ), - ) - - it.live("read with backslashes, assert with forward slashes", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const file = path.join(dir, "file.txt") - yield* put(file, "content") - yield* touch(file, 1_000) - - const forward = file.replaceAll("\\", "/") - yield* read(id, file) - yield* check(id, forward) - }), - ), - ) - - it.live("get returns timestamp regardless of slash direction", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const file = path.join(dir, "file.txt") - yield* put(file, "content") - - const forward = file.replaceAll("\\", "/") - yield* read(id, forward) - - const result = yield* get(id, file) - expect(result).toBeInstanceOf(Date) - }), - ), - ) - - it.live("withLock serializes regardless of slash direction", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const file = path.join(dir, "file.txt") - const forward = file.replaceAll("\\", "/") - const order: number[] = [] - const hold = yield* Deferred.make() - const ready = yield* Deferred.make() - - const one = yield* lock(file, () => - Effect.gen(function* () { - order.push(1) - yield* Deferred.succeed(ready, void 0) - yield* Deferred.await(hold) - order.push(2) - }), - ).pipe(Effect.forkScoped) - - yield* Deferred.await(ready) - - const two = yield* lock(forward, () => - Effect.sync(() => { - order.push(3) - order.push(4) - }), - ).pipe(Effect.forkScoped) - - yield* Deferred.succeed(hold, void 0) - yield* Fiber.join(one) - yield* Fiber.join(two) - - expect(order).toEqual([1, 2, 3, 4]) - }), - ), - ) - }) - - describe("stat() Filesystem.stat pattern", () => { - it.live("reads file modification time via Filesystem.stat()", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const file = path.join(dir, "file.txt") - yield* put(file, "content") - yield* touch(file, 1_000) - - yield* read(id, file) - - const stat = Filesystem.stat(file) - expect(stat?.mtime).toBeInstanceOf(Date) - expect(stat!.mtime.getTime()).toBeGreaterThan(0) - - yield* check(id, file) - }), - ), - ) - - it.live("detects modification via stat mtime", () => - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const file = path.join(dir, "file.txt") - yield* put(file, "original") - yield* touch(file, 1_000) - - yield* read(id, file) - - const first = Filesystem.stat(file) - - yield* put(file, "modified") - yield* touch(file, 2_000) - - const second = Filesystem.stat(file) - expect(second!.mtime.getTime()).toBeGreaterThan(first!.mtime.getTime()) - - yield* fail(check(id, file)) - }), - ), - ) - }) -}) diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 121d662e5f..2f59046840 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -7,7 +7,6 @@ import { Agent as AgentSvc } from "../../src/agent/agent" import { Bus } from "../../src/bus" import { Command } from "../../src/command" import { Config } from "../../src/config" -import { FileTime } from "../../src/file/time" import { LSP } from "../../src/lsp" import { MCP } from "../../src/mcp" import { Permission } from "../../src/permission" @@ -148,16 +147,6 @@ const lsp = Layer.succeed( }), ) -const filetime = Layer.succeed( - FileTime.Service, - FileTime.Service.of({ - read: () => Effect.void, - get: () => Effect.succeed(undefined), - assert: () => Effect.void, - withLock: (_filepath, fn) => fn(), - }), -) - const status = SessionStatus.layer.pipe(Layer.provideMerge(Bus.layer)) const run = SessionRunState.layer.pipe(Layer.provide(status)) const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer) @@ -173,7 +162,6 @@ function makeHttp() { Plugin.defaultLayer, Config.defaultLayer, ProviderSvc.defaultLayer, - filetime, lsp, mcp, AppFileSystem.defaultLayer, diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index 1f66ccb995..6517547339 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -33,7 +33,6 @@ import { Agent as AgentSvc } from "../../src/agent/agent" import { Bus } from "../../src/bus" import { Command } from "../../src/command" import { Config } from "../../src/config" -import { FileTime } from "../../src/file/time" import { LSP } from "../../src/lsp" import { MCP } from "../../src/mcp" import { Permission } from "../../src/permission" @@ -102,16 +101,6 @@ const lsp = Layer.succeed( }), ) -const filetime = Layer.succeed( - FileTime.Service, - FileTime.Service.of({ - read: () => Effect.void, - get: () => Effect.succeed(undefined), - assert: () => Effect.void, - withLock: (_filepath, fn) => fn(), - }), -) - const status = SessionStatus.layer.pipe(Layer.provideMerge(Bus.layer)) const run = SessionRunState.layer.pipe(Layer.provide(status)) const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer) @@ -128,7 +117,6 @@ function makeHttp() { Plugin.defaultLayer, Config.defaultLayer, ProviderSvc.defaultLayer, - filetime, lsp, mcp, AppFileSystem.defaultLayer, diff --git a/packages/opencode/test/tool/edit.test.ts b/packages/opencode/test/tool/edit.test.ts index 2e3dfa8a69..4759b8be36 100644 --- a/packages/opencode/test/tool/edit.test.ts +++ b/packages/opencode/test/tool/edit.test.ts @@ -5,7 +5,6 @@ import { Effect, Layer, ManagedRuntime } from "effect" import { EditTool } from "../../src/tool/edit" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" -import { FileTime } from "../../src/file/time" import { LSP } from "../../src/lsp" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Format } from "../../src/format" @@ -38,7 +37,6 @@ async function touch(file: string, time: number) { const runtime = ManagedRuntime.make( Layer.mergeAll( LSP.defaultLayer, - FileTime.defaultLayer, AppFileSystem.defaultLayer, Format.defaultLayer, Bus.layer, @@ -59,9 +57,6 @@ const resolve = () => }), ) -const readFileTime = (sessionID: SessionID, filepath: string) => - runtime.runPromise(FileTime.Service.use((ft) => ft.read(sessionID, filepath))) - const subscribeBus = (def: D, callback: () => unknown) => runtime.runPromise(Bus.Service.use((bus) => bus.subscribeCallback(def, callback))) @@ -173,8 +168,6 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - await readFileTime(ctx.sessionID, filepath) - const edit = await resolve() const result = await Effect.runPromise( edit.execute( @@ -202,8 +195,6 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - await readFileTime(ctx.sessionID, filepath) - const edit = await resolve() await expect( Effect.runPromise( @@ -254,8 +245,6 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - await readFileTime(ctx.sessionID, filepath) - const edit = await resolve() await expect( Effect.runPromise( @@ -273,65 +262,6 @@ describe("tool.edit", () => { }) }) - test("throws error when file was not read first (FileTime)", async () => { - await using tmp = await tmpdir() - const filepath = path.join(tmp.path, "file.txt") - await fs.writeFile(filepath, "content", "utf-8") - - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const edit = await resolve() - await expect( - Effect.runPromise( - edit.execute( - { - filePath: filepath, - oldString: "content", - newString: "modified", - }, - ctx, - ), - ), - ).rejects.toThrow("You must read file") - }, - }) - }) - - test("throws error when file has been modified since read", async () => { - await using tmp = await tmpdir() - const filepath = path.join(tmp.path, "file.txt") - await fs.writeFile(filepath, "original content", "utf-8") - await touch(filepath, 1_000) - - await Instance.provide({ - directory: tmp.path, - fn: async () => { - // Read first - await readFileTime(ctx.sessionID, filepath) - - // Simulate external modification - await fs.writeFile(filepath, "modified externally", "utf-8") - await touch(filepath, 2_000) - - // Try to edit with the new content - const edit = await resolve() - await expect( - Effect.runPromise( - edit.execute( - { - filePath: filepath, - oldString: "modified externally", - newString: "edited", - }, - ctx, - ), - ), - ).rejects.toThrow("modified since it was last read") - }, - }) - }) - test("replaces all occurrences with replaceAll option", async () => { await using tmp = await tmpdir() const filepath = path.join(tmp.path, "file.txt") @@ -340,8 +270,6 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - await readFileTime(ctx.sessionID, filepath) - const edit = await resolve() await Effect.runPromise( edit.execute( @@ -369,8 +297,6 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - await readFileTime(ctx.sessionID, filepath) - const { FileWatcher } = await import("../../src/file/watcher") const updated = await onceBus(FileWatcher.Event.Updated) @@ -406,8 +332,6 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - await readFileTime(ctx.sessionID, filepath) - const edit = await resolve() await Effect.runPromise( edit.execute( @@ -434,8 +358,6 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - await readFileTime(ctx.sessionID, filepath) - const edit = await resolve() await Effect.runPromise( edit.execute( @@ -487,8 +409,6 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - await readFileTime(ctx.sessionID, dirpath) - const edit = await resolve() await expect( Effect.runPromise( @@ -514,8 +434,6 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - await readFileTime(ctx.sessionID, filepath) - const edit = await resolve() const result = await Effect.runPromise( edit.execute( @@ -587,7 +505,6 @@ describe("tool.edit", () => { fn: async () => { const edit = await resolve() const filePath = path.join(tmp.path, "test.txt") - await readFileTime(ctx.sessionID, filePath) await Effect.runPromise( edit.execute( { @@ -730,8 +647,6 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - await readFileTime(ctx.sessionID, filepath) - const edit = await resolve() // Two concurrent edits @@ -746,9 +661,6 @@ describe("tool.edit", () => { ), ) - // Need to read again since FileTime tracks per-session - await readFileTime(ctx.sessionID, filepath) - const promise2 = Effect.runPromise( edit.execute( { diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index 3b32c72e05..c3d7074bfb 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -4,7 +4,6 @@ import path from "path" import { Agent } from "../../src/agent/agent" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { FileTime } from "../../src/file/time" import { LSP } from "../../src/lsp" import { Permission } from "../../src/permission" import { Instance } from "../../src/project/instance" @@ -40,7 +39,6 @@ const it = testEffect( Agent.defaultLayer, AppFileSystem.defaultLayer, CrossSpawnSpawner.defaultLayer, - FileTime.defaultLayer, Instruction.defaultLayer, LSP.defaultLayer, Truncate.defaultLayer, diff --git a/packages/opencode/test/tool/write.test.ts b/packages/opencode/test/tool/write.test.ts index 46bbe2e401..50d3b57527 100644 --- a/packages/opencode/test/tool/write.test.ts +++ b/packages/opencode/test/tool/write.test.ts @@ -6,7 +6,6 @@ import { WriteTool } from "../../src/tool/write" import { Instance } from "../../src/project/instance" import { LSP } from "../../src/lsp" import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { FileTime } from "../../src/file/time" import { Bus } from "../../src/bus" import { Format } from "../../src/format" import { Truncate } from "../../src/tool" @@ -36,7 +35,6 @@ const it = testEffect( Layer.mergeAll( LSP.defaultLayer, AppFileSystem.defaultLayer, - FileTime.defaultLayer, Bus.layer, Format.defaultLayer, CrossSpawnSpawner.defaultLayer, @@ -58,11 +56,6 @@ const run = Effect.fn("WriteToolTest.run")(function* ( return yield* tool.execute(args, next) }) -const markRead = Effect.fn("WriteToolTest.markRead")(function* (sessionID: string, filepath: string) { - const ft = yield* FileTime.Service - yield* ft.read(sessionID as any, filepath) -}) - describe("tool.write", () => { describe("new file creation", () => { it.live("writes content to new file", () => @@ -110,8 +103,6 @@ describe("tool.write", () => { Effect.gen(function* () { const filepath = path.join(dir, "existing.txt") yield* Effect.promise(() => fs.writeFile(filepath, "old content", "utf-8")) - yield* markRead(ctx.sessionID, filepath) - const result = yield* run({ filePath: filepath, content: "new content" }) expect(result.output).toContain("Wrote file successfully") @@ -128,8 +119,6 @@ describe("tool.write", () => { Effect.gen(function* () { const filepath = path.join(dir, "file.txt") yield* Effect.promise(() => fs.writeFile(filepath, "old", "utf-8")) - yield* markRead(ctx.sessionID, filepath) - const result = yield* run({ filePath: filepath, content: "new" }) expect(result.metadata).toHaveProperty("filepath", filepath) @@ -231,8 +220,6 @@ describe("tool.write", () => { const readonlyPath = path.join(dir, "readonly.txt") yield* Effect.promise(() => fs.writeFile(readonlyPath, "test", "utf-8")) yield* Effect.promise(() => fs.chmod(readonlyPath, 0o444)) - yield* markRead(ctx.sessionID, readonlyPath) - const exit = yield* run({ filePath: readonlyPath, content: "new content" }).pipe(Effect.exit) expect(exit._tag).toBe("Failure") }), diff --git a/packages/web/src/content/docs/ar/cli.mdx b/packages/web/src/content/docs/ar/cli.mdx index 826ea43040..ab2c12fb20 100644 --- a/packages/web/src/content/docs/ar/cli.mdx +++ b/packages/web/src/content/docs/ar/cli.mdx @@ -573,7 +573,6 @@ opencode upgrade v0.1.48 | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | تعطيل تحميل `.claude/skills` | | `OPENCODE_DISABLE_MODELS_FETCH` | boolean | تعطيل جلب النماذج من مصادر بعيدة | | `OPENCODE_FAKE_VCS` | string | مزود VCS وهمي لأغراض الاختبار | -| `OPENCODE_DISABLE_FILETIME_CHECK` | boolean | تعطيل التحقق من وقت الملف لتحسين الأداء | | `OPENCODE_CLIENT` | string | معرّف العميل (الافتراضي `cli`) | | `OPENCODE_ENABLE_EXA` | boolean | تفعيل أدوات بحث الويب من Exa | | `OPENCODE_SERVER_PASSWORD` | string | تفعيل المصادقة الأساسية لخادمي `serve`/`web` | diff --git a/packages/web/src/content/docs/bs/cli.mdx b/packages/web/src/content/docs/bs/cli.mdx index 979066acbc..118b81ba4e 100644 --- a/packages/web/src/content/docs/bs/cli.mdx +++ b/packages/web/src/content/docs/bs/cli.mdx @@ -571,7 +571,6 @@ OpenCode se može konfigurirati pomoću varijabli okruženja. | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Onemogući učitavanje `.claude/skills` | | `OPENCODE_DISABLE_MODELS_FETCH` | boolean | Onemogući dohvaćanje modela iz udaljenih izvora | | `OPENCODE_FAKE_VCS` | string | Lažni VCS provajder za potrebe testiranja | -| `OPENCODE_DISABLE_FILETIME_CHECK` | boolean | Onemogući provjeru vremena datoteke radi optimizacije | | `OPENCODE_CLIENT` | string | Identifikator klijenta (zadano na `cli`) | | `OPENCODE_ENABLE_EXA` | boolean | Omogući Exa alate za web pretraživanje | | `OPENCODE_SERVER_PASSWORD` | string | Omogući osnovnu autentifikaciju za `serve`/`web` | diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index 579038ad03..786b9d3d94 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -575,7 +575,6 @@ OpenCode can be configured using environment variables. | `OPENCODE_DISABLE_MODELS_FETCH` | boolean | Disable fetching models from remote sources | | `OPENCODE_DISABLE_MOUSE` | boolean | Disable mouse capture in the TUI | | `OPENCODE_FAKE_VCS` | string | Fake VCS provider for testing purposes | -| `OPENCODE_DISABLE_FILETIME_CHECK` | boolean | Disable file time checking for optimization | | `OPENCODE_CLIENT` | string | Client identifier (defaults to `cli`) | | `OPENCODE_ENABLE_EXA` | boolean | Enable Exa web search tools | | `OPENCODE_SERVER_PASSWORD` | string | Enable basic auth for `serve`/`web` | diff --git a/packages/web/src/content/docs/da/cli.mdx b/packages/web/src/content/docs/da/cli.mdx index 40c6645e67..45c4f08e3f 100644 --- a/packages/web/src/content/docs/da/cli.mdx +++ b/packages/web/src/content/docs/da/cli.mdx @@ -574,7 +574,6 @@ OpenCode kan konfigureres ved hjælp af miljøvariabler. | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Deaktiver indlæsning af `.claude/skills` | | `OPENCODE_DISABLE_MODELS_FETCH` | boolean | Deaktivering af modeller fra eksterne kilder | | `OPENCODE_FAKE_VCS` | string | Falsk VCS-udbyder til testformål | -| `OPENCODE_DISABLE_FILETIME_CHECK` | boolean | Deaktiver filtidskontrol for optimering | | `OPENCODE_CLIENT` | string | Klient-id (standard til `cli`) | | `OPENCODE_ENABLE_EXA` | boolean | Aktiver Exa-websøgeværktøjer | | `OPENCODE_SERVER_PASSWORD` | string | Aktiver grundlæggende godkendelse for `serve`/`web` | diff --git a/packages/web/src/content/docs/de/cli.mdx b/packages/web/src/content/docs/de/cli.mdx index cb1b974e10..43a1189d60 100644 --- a/packages/web/src/content/docs/de/cli.mdx +++ b/packages/web/src/content/docs/de/cli.mdx @@ -573,7 +573,6 @@ OpenCode kann mithilfe von Umgebungsvariablen konfiguriert werden. | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolescher Wert | Deaktivieren Sie das Laden von `.claude/skills` | | `OPENCODE_DISABLE_MODELS_FETCH` | boolescher Wert | Deaktivieren Sie das Abrufen von Modellen aus Remote-Quellen | | `OPENCODE_FAKE_VCS` | Zeichenfolge | Gefälschter VCS-Anbieter zu Testzwecken | -| `OPENCODE_DISABLE_FILETIME_CHECK` | boolescher Wert | Dateizeitprüfung zur Optimierung deaktivieren | | `OPENCODE_CLIENT` | Zeichenfolge | Client-ID (standardmäßig `cli`) | | `OPENCODE_ENABLE_EXA` | boolescher Wert | Exa-Websuchtools aktivieren | | `OPENCODE_SERVER_PASSWORD` | Zeichenfolge | Aktivieren Sie die Basisauthentifizierung für `serve`/`web` | diff --git a/packages/web/src/content/docs/es/cli.mdx b/packages/web/src/content/docs/es/cli.mdx index 658be27084..5c86474a61 100644 --- a/packages/web/src/content/docs/es/cli.mdx +++ b/packages/web/src/content/docs/es/cli.mdx @@ -573,7 +573,6 @@ OpenCode se puede configurar mediante variables de entorno. | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | booleano | Deshabilitar la carga `.claude/skills` | | `OPENCODE_DISABLE_MODELS_FETCH` | booleano | Deshabilitar la recuperación de modelos desde fuentes remotas | | `OPENCODE_FAKE_VCS` | cadena | Proveedor de VCS falso para fines de prueba | -| `OPENCODE_DISABLE_FILETIME_CHECK` | booleano | Deshabilite la verificación del tiempo del archivo para optimizarlo | | `OPENCODE_CLIENT` | cadena | Identificador de cliente (por defecto `cli`) | | `OPENCODE_ENABLE_EXA` | booleano | Habilitar las herramientas de búsqueda web de Exa | | `OPENCODE_SERVER_PASSWORD` | cadena | Habilite la autenticación básica para `serve`/`web` | diff --git a/packages/web/src/content/docs/fr/cli.mdx b/packages/web/src/content/docs/fr/cli.mdx index 2c763618e4..cffa748ad2 100644 --- a/packages/web/src/content/docs/fr/cli.mdx +++ b/packages/web/src/content/docs/fr/cli.mdx @@ -574,7 +574,6 @@ OpenCode peut être configuré à l'aide de variables d'environnement. | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | booléen | Désactiver le chargement de `.claude/skills` | | `OPENCODE_DISABLE_MODELS_FETCH` | booléen | Désactiver la récupération de modèles à partir de sources distantes | | `OPENCODE_FAKE_VCS` | chaîne | Faux fournisseur VCS à des fins de test | -| `OPENCODE_DISABLE_FILETIME_CHECK` | booléen | Désactiver la vérification de l'heure des fichiers pour l'optimisation | | `OPENCODE_CLIENT` | chaîne | Identifiant du client (par défaut `cli`) | | `OPENCODE_ENABLE_EXA` | booléen | Activer les outils de recherche Web Exa | | `OPENCODE_SERVER_PASSWORD` | chaîne | Activer l'authentification de base pour `serve`/`web` | diff --git a/packages/web/src/content/docs/it/cli.mdx b/packages/web/src/content/docs/it/cli.mdx index 46d7da1495..952dfba090 100644 --- a/packages/web/src/content/docs/it/cli.mdx +++ b/packages/web/src/content/docs/it/cli.mdx @@ -574,7 +574,6 @@ OpenCode può essere configurato tramite variabili d'ambiente. | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Disabilita caricamento di `.claude/skills` | | `OPENCODE_DISABLE_MODELS_FETCH` | boolean | Disabilita fetch dei modelli da fonti remote | | `OPENCODE_FAKE_VCS` | string | Provider VCS finto per scopi di test | -| `OPENCODE_DISABLE_FILETIME_CHECK` | boolean | Disabilita controllo file time per ottimizzazione | | `OPENCODE_CLIENT` | string | Identificatore client (default `cli`) | | `OPENCODE_ENABLE_EXA` | boolean | Abilita gli strumenti di web search Exa | | `OPENCODE_SERVER_PASSWORD` | string | Abilita basic auth per `serve`/`web` | diff --git a/packages/web/src/content/docs/ja/cli.mdx b/packages/web/src/content/docs/ja/cli.mdx index f690c7d7e9..82a8852ea5 100644 --- a/packages/web/src/content/docs/ja/cli.mdx +++ b/packages/web/src/content/docs/ja/cli.mdx @@ -573,7 +573,6 @@ OpenCode は環境変数を使用して構成できます。 | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | ブール値 | `.claude/skills` のロードを無効にする | | `OPENCODE_DISABLE_MODELS_FETCH` | ブール値 | リモートソースからのモデルの取得を無効にする | | `OPENCODE_FAKE_VCS` | 文字列 | テスト目的の偽の VCS プロバイダー | -| `OPENCODE_DISABLE_FILETIME_CHECK` | ブール値 | 最適化のためにファイル時間チェックを無効にする | | `OPENCODE_CLIENT` | 文字列 | クライアント識別子 (デフォルトは `cli`) | | `OPENCODE_ENABLE_EXA` | ブール値 | Exa Web 検索ツールを有効にする | | `OPENCODE_SERVER_PASSWORD` | 文字列 | `serve`/`web` の基本認証を有効にする | diff --git a/packages/web/src/content/docs/ko/cli.mdx b/packages/web/src/content/docs/ko/cli.mdx index 0562ab8afd..b0ce10567e 100644 --- a/packages/web/src/content/docs/ko/cli.mdx +++ b/packages/web/src/content/docs/ko/cli.mdx @@ -573,7 +573,6 @@ OpenCode는 환경 변수로도 구성할 수 있습니다. | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | `.claude/skills` 로드 비활성화 | | `OPENCODE_DISABLE_MODELS_FETCH` | boolean | 원격 소스에서 모델 목록 가져오기 비활성화 | | `OPENCODE_FAKE_VCS` | string | 테스트용 가짜 VCS provider | -| `OPENCODE_DISABLE_FILETIME_CHECK` | boolean | 최적화를 위한 파일 시간 검사 비활성화 | | `OPENCODE_CLIENT` | string | 클라이언트 식별자(기본값: `cli`) | | `OPENCODE_ENABLE_EXA` | boolean | Exa 웹 검색 도구 활성화 | | `OPENCODE_SERVER_PASSWORD` | string | `serve`/`web` 기본 인증 활성화 | diff --git a/packages/web/src/content/docs/nb/cli.mdx b/packages/web/src/content/docs/nb/cli.mdx index 8b6d283e10..8312a1a7c5 100644 --- a/packages/web/src/content/docs/nb/cli.mdx +++ b/packages/web/src/content/docs/nb/cli.mdx @@ -574,7 +574,6 @@ OpenCode kan konfigureres ved hjelp av miljøvariabler. | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolsk | Deaktiver innlasting av `.claude/skills` | | `OPENCODE_DISABLE_MODELS_FETCH` | boolsk | Deaktiver henting av modeller fra eksterne kilder | | `OPENCODE_FAKE_VCS` | streng | Falsk VCS-leverandør for testformål | -| `OPENCODE_DISABLE_FILETIME_CHECK` | boolsk | Deaktiver filtidskontroll for optimalisering | | `OPENCODE_CLIENT` | streng | Klientidentifikator (standard til `cli`) | | `OPENCODE_ENABLE_EXA` | boolsk | Aktiver Exa-nettsøkeverktøy | | `OPENCODE_SERVER_PASSWORD` | streng | Aktiver grunnleggende autentisering for `serve`/`web` | diff --git a/packages/web/src/content/docs/pl/cli.mdx b/packages/web/src/content/docs/pl/cli.mdx index 6cdc67a48f..3931cd6467 100644 --- a/packages/web/src/content/docs/pl/cli.mdx +++ b/packages/web/src/content/docs/pl/cli.mdx @@ -574,7 +574,6 @@ OpenCode można skonfigurować za pomocą zmiennych środowiskowych. | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Wyłącz ładowanie `.claude/skills` | | `OPENCODE_DISABLE_MODELS_FETCH` | boolean | Wyłącz pobieranie modeli ze źródeł zewnętrznych | | `OPENCODE_FAKE_VCS` | string | Fałszywy dostawca VCS do celów testowych | -| `OPENCODE_DISABLE_FILETIME_CHECK` | boolean | Wyłącz sprawdzanie czasu modyfikacji plików (optymalizacja) | | `OPENCODE_CLIENT` | string | Identyfikator klienta (domyślnie `cli`) | | `OPENCODE_ENABLE_EXA` | boolean | Włącz narzędzie wyszukiwania internetowego Exa | | `OPENCODE_SERVER_PASSWORD` | string | Włącz uwierzytelnianie podstawowe dla `serve`/`web` | diff --git a/packages/web/src/content/docs/pt-br/cli.mdx b/packages/web/src/content/docs/pt-br/cli.mdx index 32c50d7c0a..78190b3c5d 100644 --- a/packages/web/src/content/docs/pt-br/cli.mdx +++ b/packages/web/src/content/docs/pt-br/cli.mdx @@ -573,7 +573,6 @@ O opencode pode ser configurado usando variáveis de ambiente. | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Desabilitar carregamento de `.claude/skills` | | `OPENCODE_DISABLE_MODELS_FETCH` | boolean | Desabilitar busca de modelos de fontes remotas | | `OPENCODE_FAKE_VCS` | string | Provedor VCS falso para fins de teste | -| `OPENCODE_DISABLE_FILETIME_CHECK` | boolean | Desabilitar verificação de tempo de arquivo para otimização | | `OPENCODE_CLIENT` | string | Identificador do cliente (padrão é `cli`) | | `OPENCODE_ENABLE_EXA` | boolean | Habilitar ferramentas de busca web Exa | | `OPENCODE_SERVER_PASSWORD` | string | Habilitar autenticação básica para `serve`/`web` | diff --git a/packages/web/src/content/docs/ru/cli.mdx b/packages/web/src/content/docs/ru/cli.mdx index a98111530f..f5aeee256f 100644 --- a/packages/web/src/content/docs/ru/cli.mdx +++ b/packages/web/src/content/docs/ru/cli.mdx @@ -574,7 +574,6 @@ opencode можно настроить с помощью переменных с | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | логическое значение | Отключить загрузку `.claude/skills` | | `OPENCODE_DISABLE_MODELS_FETCH` | логическое значение | Отключить получение моделей из удаленных источников | | `OPENCODE_FAKE_VCS` | строка | Поддельный поставщик VCS для целей тестирования | -| `OPENCODE_DISABLE_FILETIME_CHECK` | логическое значение | Отключить проверку времени файла для оптимизации | | `OPENCODE_CLIENT` | строка | Идентификатор клиента (по умолчанию `cli`) | | `OPENCODE_ENABLE_EXA` | логическое значение | Включить инструменты веб-поиска Exa | | `OPENCODE_SERVER_PASSWORD` | строка | Включить базовую аутентификацию для `serve`/`web` | diff --git a/packages/web/src/content/docs/th/cli.mdx b/packages/web/src/content/docs/th/cli.mdx index 2f75a96a7e..4b2db9d988 100644 --- a/packages/web/src/content/docs/th/cli.mdx +++ b/packages/web/src/content/docs/th/cli.mdx @@ -575,7 +575,6 @@ OpenCode สามารถกำหนดค่าโดยใช้ตัว | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | Boolean | ปิดใช้งานการนำเข้า `.claude/skills` | | `OPENCODE_DISABLE_MODELS_FETCH` | Boolean | ปิดใช้งานการดึงรายการโมเดลจากระยะไกล | | `OPENCODE_FAKE_VCS` | String | เปิดใช้งาน VCS จำลองสำหรับการทดสอบ | -| `OPENCODE_DISABLE_FILETIME_CHECK` | Boolean | ปิดใช้งานการตรวจสอบเวลาแก้ไขไฟล์ | | `OPENCODE_CLIENT` | String | ตัวระบุไคลเอนต์ (ค่าเริ่มต้นคือ `cli`) | | `OPENCODE_ENABLE_EXA` | Boolean | เปิดใช้งานการใช้ Exa แทน ls หากมี | | `OPENCODE_SERVER_PASSWORD` | String | รหัสผ่านสำหรับการตรวจสอบสิทธิ์พื้นฐาน `serve`/`web` | diff --git a/packages/web/src/content/docs/tr/cli.mdx b/packages/web/src/content/docs/tr/cli.mdx index 41600b5bf0..75ecca9926 100644 --- a/packages/web/src/content/docs/tr/cli.mdx +++ b/packages/web/src/content/docs/tr/cli.mdx @@ -574,7 +574,6 @@ opencode ortam değişkenleri kullanılarak yapılandırılabilir. | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | `.claude/skills` yüklemesini devre dışı bırak | | `OPENCODE_DISABLE_MODELS_FETCH` | boolean | Uzak kaynaklardan model getirmeyi devre dışı bırakın | | `OPENCODE_FAKE_VCS` | string | Test amaçlı sahte VCS sağlayıcısı | -| `OPENCODE_DISABLE_FILETIME_CHECK` | boolean | Optimizasyon için dosya süresi kontrolünü devre dışı bırakın | | `OPENCODE_CLIENT` | string | Client kimliği (varsayılan: `cli`) | | `OPENCODE_ENABLE_EXA` | boolean | Exa web arama araçlarını etkinleştir | | `OPENCODE_SERVER_PASSWORD` | string | `serve`/`web` için temel kimlik doğrulamayı etkinleştirin | diff --git a/packages/web/src/content/docs/zh-cn/cli.mdx b/packages/web/src/content/docs/zh-cn/cli.mdx index 0c54d3d7b1..c0cff134a5 100644 --- a/packages/web/src/content/docs/zh-cn/cli.mdx +++ b/packages/web/src/content/docs/zh-cn/cli.mdx @@ -574,7 +574,6 @@ OpenCode 可以通过环境变量进行配置。 | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | 禁用加载 `.claude/skills` | | `OPENCODE_DISABLE_MODELS_FETCH` | boolean | 禁用从远程源获取模型 | | `OPENCODE_FAKE_VCS` | string | 用于测试目的的模拟 VCS 提供商 | -| `OPENCODE_DISABLE_FILETIME_CHECK` | boolean | 禁用文件时间检查优化 | | `OPENCODE_CLIENT` | string | 客户端标识符(默认为 `cli`) | | `OPENCODE_ENABLE_EXA` | boolean | 启用 Exa 网络搜索工具 | | `OPENCODE_SERVER_PASSWORD` | string | 为 `serve`/`web` 启用基本认证 | diff --git a/packages/web/src/content/docs/zh-tw/cli.mdx b/packages/web/src/content/docs/zh-tw/cli.mdx index 5de2b96375..4df9d13fdd 100644 --- a/packages/web/src/content/docs/zh-tw/cli.mdx +++ b/packages/web/src/content/docs/zh-tw/cli.mdx @@ -574,7 +574,6 @@ OpenCode 可以透過環境變數進行設定。 | `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | 停用載入 `.claude/skills` | | `OPENCODE_DISABLE_MODELS_FETCH` | boolean | 停用從遠端來源擷取模型 | | `OPENCODE_FAKE_VCS` | string | 用於測試目的的模擬 VCS 供應商 | -| `OPENCODE_DISABLE_FILETIME_CHECK` | boolean | 停用檔案時間檢查最佳化 | | `OPENCODE_CLIENT` | string | 用戶端識別碼(預設為 `cli`) | | `OPENCODE_ENABLE_EXA` | boolean | 啟用 Exa 網路搜尋工具 | | `OPENCODE_SERVER_PASSWORD` | string | 為 `serve`/`web` 啟用基本認證 | From f0caeb9b25ae4f57232a152a4e1c8bdd823e2f65 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 04:32:17 +0000 Subject: [PATCH 071/335] chore: generate --- packages/web/src/content/docs/pl/cli.mdx | 52 ++++++++++++------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/web/src/content/docs/pl/cli.mdx b/packages/web/src/content/docs/pl/cli.mdx index 3931cd6467..e175870cbf 100644 --- a/packages/web/src/content/docs/pl/cli.mdx +++ b/packages/web/src/content/docs/pl/cli.mdx @@ -553,32 +553,32 @@ Interfejs CLI OpenCode przyjmuje następujące flagi globalne dla każdego polec OpenCode można skonfigurować za pomocą zmiennych środowiskowych. -| Zmienna | Typ | Opis | -| ------------------------------------- | ------- | ----------------------------------------------------------- | -| `OPENCODE_AUTO_SHARE` | boolean | Automatycznie udostępniaj sesje | -| `OPENCODE_GIT_BASH_PATH` | string | Ścieżka do pliku wykonywalnego Git Bash w systemie Windows | -| `OPENCODE_CONFIG` | string | Ścieżka do pliku konfiguracyjnego | -| `OPENCODE_TUI_CONFIG` | string | Ścieżka do pliku konfiguracyjnego TUI | -| `OPENCODE_CONFIG_DIR` | string | Ścieżka do katalogu konfiguracyjnego | -| `OPENCODE_CONFIG_CONTENT` | string | Treść konfiguracji JSON (inline) | -| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Wyłącz automatyczne sprawdzanie aktualizacji | -| `OPENCODE_DISABLE_PRUNE` | boolean | Wyłącz czyszczenie starych wyników (pruning) | -| `OPENCODE_DISABLE_TERMINAL_TITLE` | boolean | Wyłącz automatyczne ustawianie tytułu terminala | -| `OPENCODE_PERMISSION` | string | Konfiguracja uprawnień w JSON (inline) | -| `OPENCODE_DISABLE_DEFAULT_PLUGINS` | boolean | Wyłącz domyślne wtyczki | -| `OPENCODE_DISABLE_LSP_DOWNLOAD` | boolean | Wyłącz automatyczne pobieranie serwerów LSP | -| `OPENCODE_ENABLE_EXPERIMENTAL_MODELS` | boolean | Włącz modele eksperymentalne | -| `OPENCODE_DISABLE_AUTOCOMPACT` | boolean | Wyłącz automatyczne kompaktowanie kontekstu | -| `OPENCODE_DISABLE_CLAUDE_CODE` | boolean | Wyłącz integrację z `.claude` (prompt + skills) | -| `OPENCODE_DISABLE_CLAUDE_CODE_PROMPT` | boolean | Wyłącz czytanie `~/.claude/CLAUDE.md` | -| `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Wyłącz ładowanie `.claude/skills` | -| `OPENCODE_DISABLE_MODELS_FETCH` | boolean | Wyłącz pobieranie modeli ze źródeł zewnętrznych | -| `OPENCODE_FAKE_VCS` | string | Fałszywy dostawca VCS do celów testowych | -| `OPENCODE_CLIENT` | string | Identyfikator klienta (domyślnie `cli`) | -| `OPENCODE_ENABLE_EXA` | boolean | Włącz narzędzie wyszukiwania internetowego Exa | -| `OPENCODE_SERVER_PASSWORD` | string | Włącz uwierzytelnianie podstawowe dla `serve`/`web` | -| `OPENCODE_SERVER_USERNAME` | string | Nazwa użytkownika do autoryzacji (domyślnie `opencode`) | -| `OPENCODE_MODELS_URL` | string | Niestandardowy adres URL do pobierania konfiguracji modeli | +| Zmienna | Typ | Opis | +| ------------------------------------- | ------- | ---------------------------------------------------------- | +| `OPENCODE_AUTO_SHARE` | boolean | Automatycznie udostępniaj sesje | +| `OPENCODE_GIT_BASH_PATH` | string | Ścieżka do pliku wykonywalnego Git Bash w systemie Windows | +| `OPENCODE_CONFIG` | string | Ścieżka do pliku konfiguracyjnego | +| `OPENCODE_TUI_CONFIG` | string | Ścieżka do pliku konfiguracyjnego TUI | +| `OPENCODE_CONFIG_DIR` | string | Ścieżka do katalogu konfiguracyjnego | +| `OPENCODE_CONFIG_CONTENT` | string | Treść konfiguracji JSON (inline) | +| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Wyłącz automatyczne sprawdzanie aktualizacji | +| `OPENCODE_DISABLE_PRUNE` | boolean | Wyłącz czyszczenie starych wyników (pruning) | +| `OPENCODE_DISABLE_TERMINAL_TITLE` | boolean | Wyłącz automatyczne ustawianie tytułu terminala | +| `OPENCODE_PERMISSION` | string | Konfiguracja uprawnień w JSON (inline) | +| `OPENCODE_DISABLE_DEFAULT_PLUGINS` | boolean | Wyłącz domyślne wtyczki | +| `OPENCODE_DISABLE_LSP_DOWNLOAD` | boolean | Wyłącz automatyczne pobieranie serwerów LSP | +| `OPENCODE_ENABLE_EXPERIMENTAL_MODELS` | boolean | Włącz modele eksperymentalne | +| `OPENCODE_DISABLE_AUTOCOMPACT` | boolean | Wyłącz automatyczne kompaktowanie kontekstu | +| `OPENCODE_DISABLE_CLAUDE_CODE` | boolean | Wyłącz integrację z `.claude` (prompt + skills) | +| `OPENCODE_DISABLE_CLAUDE_CODE_PROMPT` | boolean | Wyłącz czytanie `~/.claude/CLAUDE.md` | +| `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Wyłącz ładowanie `.claude/skills` | +| `OPENCODE_DISABLE_MODELS_FETCH` | boolean | Wyłącz pobieranie modeli ze źródeł zewnętrznych | +| `OPENCODE_FAKE_VCS` | string | Fałszywy dostawca VCS do celów testowych | +| `OPENCODE_CLIENT` | string | Identyfikator klienta (domyślnie `cli`) | +| `OPENCODE_ENABLE_EXA` | boolean | Włącz narzędzie wyszukiwania internetowego Exa | +| `OPENCODE_SERVER_PASSWORD` | string | Włącz uwierzytelnianie podstawowe dla `serve`/`web` | +| `OPENCODE_SERVER_USERNAME` | string | Nazwa użytkownika do autoryzacji (domyślnie `opencode`) | +| `OPENCODE_MODELS_URL` | string | Niestandardowy adres URL do pobierania konfiguracji modeli | --- From 72d7cb717d5b2994a525257cd7fccf0533ca34ee Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 17 Apr 2026 00:42:45 -0400 Subject: [PATCH 072/335] remove accidental commit of daytona plugin (#23030) --- .opencode/opencode.jsonc | 1 - bun.lock | 333 +-------------------------------------- daytona.ts | 197 ----------------------- package.json | 1 - 4 files changed, 8 insertions(+), 524 deletions(-) delete mode 100644 daytona.ts diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index 91b7abdbc6..8380f7f719 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -10,7 +10,6 @@ "packages/opencode/migration/*": "deny", }, }, - "plugin": ["../daytona.ts"], "mcp": {}, "tools": { "github-triage": false, diff --git a/bun.lock b/bun.lock index 7c562c4d4b..c22a2ff270 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,6 @@ "name": "opencode", "dependencies": { "@aws-sdk/client-s3": "3.933.0", - "@daytona/sdk": "0.167.0", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", @@ -865,8 +864,6 @@ "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.993.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.993.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-cognito-identity": "^3.972.3", "@aws-sdk/credential-provider-env": "^3.972.9", "@aws-sdk/credential-provider-http": "^3.972.11", "@aws-sdk/credential-provider-ini": "^3.972.9", "@aws-sdk/credential-provider-login": "^3.972.9", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/credential-provider-process": "^3.972.9", "@aws-sdk/credential-provider-sso": "^3.972.9", "@aws-sdk/credential-provider-web-identity": "^3.972.9", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-1M/nukgPSLqe9krzOKHnE8OylUaKAiokAV3xRLdeExVHcRE7WG5uzCTKWTj1imKvPjDqXq/FWhlbbdWIn7xIwA=="], - "@aws-sdk/lib-storage": ["@aws-sdk/lib-storage@3.1031.0", "", { "dependencies": { "@smithy/middleware-endpoint": "^4.4.30", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "buffer": "5.6.0", "events": "3.3.0", "stream-browserify": "3.0.0", "tslib": "^2.6.2" }, "peerDependencies": { "@aws-sdk/client-s3": "^3.1031.0" } }, "sha512-QC6MwdeXWZt9wnOV6/rgxSnVUcDeoHr5TCnCm0UPuSQOwuLX1/2QJa6oo7PdfdHg+i3G+VOnmG8QZ3tYxvBGRA=="], - "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@aws-sdk/util-arn-parser": "3.893.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-cnCLWeKPYgvV4yRYPFH6pWMdUByvu2cy2BAlfsPpvnm4RaVioztyvxmQj5PmVN5fvWs5w/2d6U7le8X9iye2sA=="], "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-5HEQ+JU4DrLNWeY27wKg/jeVa8Suy62ivJHOSUf6e6hZdVIMx0h/kXS1fHEQNNiLu2IzSEP/bFXsKBaW7x7s0g=="], @@ -1051,12 +1048,6 @@ "@ctrl/tinycolor": ["@ctrl/tinycolor@4.2.0", "", {}, "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A=="], - "@daytona/api-client": ["@daytona/api-client@0.167.0", "", { "dependencies": { "axios": "^1.6.1" } }, "sha512-2H1NhSu5GVVfg2oU9Mgt3VZRz7OUqACjBSb9GNNyC7uGPCnQh2C93Imqj2wfyUx/XF346hYpk+Nee+/NGqn3KA=="], - - "@daytona/sdk": ["@daytona/sdk@0.167.0", "", { "dependencies": { "@aws-sdk/client-s3": "^3.787.0", "@aws-sdk/lib-storage": "^3.798.0", "@daytona/api-client": "0.167.0", "@daytona/toolbox-api-client": "0.167.0", "@iarna/toml": "^2.2.5", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.207.0", "@opentelemetry/instrumentation-http": "^0.207.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-node": "^0.207.0", "@opentelemetry/sdk-trace-base": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0", "axios": "^1.13.5", "busboy": "^1.0.0", "dotenv": "^17.0.1", "expand-tilde": "^2.0.2", "fast-glob": "^3.3.0", "form-data": "^4.0.4", "isomorphic-ws": "^5.0.0", "pathe": "^2.0.3", "shell-quote": "^1.8.2", "tar": "^7.5.11" } }, "sha512-Xsb1bJ52tahpCiq2pEJdFVqqxLQoXO8laa2BFJFrSyLnGBMJXMaEormcgZfoOC9J6mVo9oOqTABLuoT+4DP11A=="], - - "@daytona/toolbox-api-client": ["@daytona/toolbox-api-client@0.167.0", "", { "dependencies": { "axios": "^1.6.1" } }, "sha512-FzSh6UVA8ptktNrRhIJCVfZMOcbXlVazTXej+YQ1rEj7L/PnGCQlBbT296xYj94C3wXQUTIxTZVkWzB4vcDTxw=="], - "@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="], "@dimforge/rapier2d-simd-compat": ["@dimforge/rapier2d-simd-compat@0.17.3", "", {}, "sha512-bijvwWz6NHsNj5e5i1vtd3dU2pDhthSaTUZSh14DUGGKJfw8eMnlWZsxwHBxB/a3AXVNDjL9abuHw1k9FGR+jg=="], @@ -1207,10 +1198,6 @@ "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], - "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="], - - "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], - "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="], "@hey-api/codegen-core": ["@hey-api/codegen-core@0.5.5", "", { "dependencies": { "@hey-api/types": "0.1.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-f2ZHucnA2wBGAY8ipB4wn/mrEYW+WUxU2huJmUvfDO6AE2vfILSHeF3wCO39Pz4wUYPoAWZByaauftLrOfC12Q=="], @@ -1229,8 +1216,6 @@ "@hono/zod-validator": ["@hono/zod-validator@0.4.2", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-1rrlBg+EpDPhzOV4hT9pxr5+xDVmKuz6YJl+la7VCwK6ass5ldyKm5fD+umJdV2zhHD6jROoCCv8NbTwyfhT0g=="], - "@iarna/toml": ["@iarna/toml@2.2.5", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="], - "@ibm/plex": ["@ibm/plex@6.4.1", "", { "dependencies": { "@ibm/telemetry-js": "^1.5.1" } }, "sha512-fnsipQywHt3zWvsnlyYKMikcVI7E2fEwpiPnIHFqlbByXVfQfANAAeJk1IV4mNnxhppUIDlhU0TzwYwL++Rn2g=="], "@ibm/telemetry-js": ["@ibm/telemetry-js@1.11.0", "", { "bin": { "ibmtelemetry": "dist/collect.js" } }, "sha512-RO/9j+URJnSfseWg9ZkEX9p+a3Ousd33DBU7rOafoZB08RqdzxFVYJ2/iM50dkBuD0o7WX7GYt1sLbNgCoE+pA=="], @@ -1361,8 +1346,6 @@ "@js-joda/core": ["@js-joda/core@5.7.0", "", {}, "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg=="], - "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], - "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], @@ -1591,56 +1574,24 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="], "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ=="], "@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="], - "@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-grpc-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/sdk-logs": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-K92RN+kQGTMzFDsCzsYNGqOsXRUnko/Ckk+t/yPJao72MewOLgBUTWVHhebgkNfRCYqDz1v3K0aPT9OJkemvgg=="], - - "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/sdk-logs": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-JpOh7MguEUls8eRfkVVW3yRhClo5b9LqwWTOg8+i4gjr/+8eiCtquJnC7whvpTIGyff06cLZ2NsEj+CVP3Mjeg=="], - - "@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-RQJEV/K6KPbQrIUbsrRkEe0ufks1o5OGLHy6jbDD8tRjeCsbFHWfg99lYBRqBV33PYZJXsigqMaAbjWGTFYzLw=="], - - "@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-metrics-otlp-http": "0.207.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-grpc-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-6flX89W54gkwmqYShdcTBR1AEF5C1Ob0O8pDgmLPikTKyEv27lByr9yBmO5WrP0+5qJuNPHrLfgFQFYi6npDGA=="], - - "@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fG8FAJmvXOrKXGIRN8+y41U41IfVXxPRVwyB05LoMqYSjugx/FSBkMZUZXUT/wclTdmBKtS5MKoi0bEKkmRhSw=="], - - "@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-metrics-otlp-http": "0.207.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kDBxiTeQjaRlUQzS1COT9ic+et174toZH6jxaVuVAvGqmxOkgjpLOjrI5ff8SMMQE69r03L3Ll3nPKekLopLwg=="], - - "@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Y5p1s39FvIRmU+F1++j7ly8/KSqhMmn6cMfpQqiDCqDjdDHwUtSq0XI0WwL3HYGnZeaR/VV4BNmsYQJ7GAPrhw=="], - - "@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-grpc-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-7u2ZmcIx6D4KG/+5np4X2qA0o+O0K8cnUDhR4WI/vr5ZZ0la9J9RG+tkSjC7Yz+2XgL6760gSIM7/nyd3yaBLA=="], - "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw=="], - "@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ruUQB4FkWtxHjNmSXjrhmJZFvyMm+tBzHyMm7YPQshApy4wvZUTcrpPyP/A/rCl/8M4BwoVIZdiwijMdbZaq4w=="], - - "@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-VV4QzhGCT7cWrGasBWxelBjqbNBbyHicWWS/66KoZoe9BzYwFB72SH2/kkc4uAviQlO8iwv2okIJy+/jqqEHTg=="], - - "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], - - "@opentelemetry/instrumentation-http": ["@opentelemetry/instrumentation-http@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/instrumentation": "0.207.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FC4i5hVixTzuhg4SV2ycTEAYx+0E2hm+GwbdoVPSA6kna0pPVI4etzaA9UkpJ9ussumQheFXP6rkGIaFJjMxsw=="], - - "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], - - "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eKFjKNdsPed4q9yYqeI5gBTLjXxDM/8jwhiC0icw3zKxHVGBySoDsed5J5q/PGY/3quzenTr3FiTxA3NiNT+nw=="], + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="], "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="], - "@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-9CrbTLFi5Ee4uepxg2qlpQIozoJuoAZU5sKMx0Mn7Oh+p7UrgCiEV6C02FOxxdYVRRFQVCinYR8Kf6eMSQsIsw=="], - - "@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FfeOHOrdhiNzecoB1jZKp2fybqmqMPJUXe2ZOydP7QzmTPYcfPeuaclTLYVhK3HyJf71kt8sTl92nV4YIaLaKA=="], - "@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="], "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], - "@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.207.0", "@opentelemetry/exporter-logs-otlp-http": "0.207.0", "@opentelemetry/exporter-logs-otlp-proto": "0.207.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.207.0", "@opentelemetry/exporter-metrics-otlp-http": "0.207.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.207.0", "@opentelemetry/exporter-prometheus": "0.207.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.207.0", "@opentelemetry/exporter-trace-otlp-http": "0.207.0", "@opentelemetry/exporter-trace-otlp-proto": "0.207.0", "@opentelemetry/exporter-zipkin": "2.2.0", "@opentelemetry/instrumentation": "0.207.0", "@opentelemetry/propagator-b3": "2.2.0", "@opentelemetry/propagator-jaeger": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "@opentelemetry/sdk-trace-node": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-hnRsX/M8uj0WaXOBvFenQ8XsE8FLVh2uSnn1rkWu4mx+qu7EKGUZvZng6y/95cyzsqOfiaDDr08Ek4jppkIDNg=="], - "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="], @@ -2555,8 +2506,6 @@ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], @@ -2779,8 +2728,6 @@ "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], - "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], "c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="], @@ -2841,8 +2788,6 @@ "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], - "cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], - "classnames": ["classnames@2.3.2", "", {}, "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="], "clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="], @@ -3055,7 +3000,7 @@ "dot-prop": ["dot-prop@8.0.2", "", { "dependencies": { "type-fest": "^3.8.0" } }, "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ=="], - "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], @@ -3207,8 +3152,6 @@ "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], - "expand-tilde": ["expand-tilde@2.0.2", "", { "dependencies": { "homedir-polyfill": "^1.0.1" } }, "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw=="], - "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], @@ -3311,8 +3254,6 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="], - "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], "framer-motion": ["framer-motion@8.5.5", "", { "dependencies": { "@motionone/dom": "^10.15.3", "hey-listen": "^1.0.8", "tslib": "^2.4.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-5IDx5bxkjWHWUF3CVJoSyUVOtrbAxtzYBBowRE2uYI/6VYhkEBD+rbTHEGuUmbGHRj6YqqSfoG7Aa1cLyWCrBA=="], @@ -3469,8 +3410,6 @@ "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="], - "homedir-polyfill": ["homedir-polyfill@1.0.3", "", { "dependencies": { "parse-passwd": "^1.0.0" } }, "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA=="], - "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], "hono-openapi": ["hono-openapi@1.1.2", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-toUcO60MftRBxqcVyxsHNYs2m4vf4xkQaiARAucQx3TiBPDtMNNkoh+C4I1vAretQZiGyaLOZNWn1YxfSyUA5g=="], @@ -3525,8 +3464,6 @@ "immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], - "import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], - "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="], "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], @@ -3789,8 +3726,6 @@ "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], - "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], - "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], @@ -4021,8 +3956,6 @@ "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], - "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], - "morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="], "motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="], @@ -4229,8 +4162,6 @@ "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="], - "parse-passwd": ["parse-passwd@1.0.0", "", {}, "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q=="], - "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], @@ -4511,8 +4442,6 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], - "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="], @@ -4755,12 +4684,8 @@ "storybook-solidjs-vite": ["storybook-solidjs-vite@10.0.12", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.7.0", "@storybook/builder-vite": "^10.3.1", "@storybook/global": "^5.0.0", "vite-plugin-solid": "^2.11.11" }, "peerDependencies": { "solid-js": "^1.9.0", "storybook": "^0.0.0-0 || ^10.0.0", "typescript": "^4.0.0 || ^5.0.0 || ^6.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["typescript"] }, "sha512-KfKhJRdxbhFLHkBzLKSEk5sO2M/+KV9cdpki5Xdl5pwNP8kcoQnZ3b/okZk8dMRV6x19j86bKc7zDfc5bPSMwA=="], - "stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="], - "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], - "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], - "streamx": ["streamx@2.25.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="], "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -5175,7 +5100,7 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], @@ -5411,16 +5336,6 @@ "@aws-sdk/credential-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="], - "@aws-sdk/lib-storage/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.30", "", { "dependencies": { "@smithy/core": "^3.23.15", "@smithy/middleware-serde": "^4.2.18", "@smithy/node-config-provider": "^4.3.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" } }, "sha512-qS2XqhKeXmdZ4nEQ4cOxIczSP/Y91wPAHYuRwmWDCh975B7/57uxsm5d6sisnUThn2u2FwzMdJNM7AbO1YPsPg=="], - - "@aws-sdk/lib-storage/@smithy/protocol-http": ["@smithy/protocol-http@5.3.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ=="], - - "@aws-sdk/lib-storage/@smithy/smithy-client": ["@smithy/smithy-client@4.12.11", "", { "dependencies": { "@smithy/core": "^3.23.15", "@smithy/middleware-endpoint": "^4.4.30", "@smithy/middleware-stack": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-stream": "^4.5.23", "tslib": "^2.6.2" } }, "sha512-wzz/Wa1CH/Tlhxh0s4DQPEcXSxSVfJ59AZcUh9Gu0c6JTlKuwGf4o/3P2TExv0VbtPFt8odIBG+eQGK2+vTECg=="], - - "@aws-sdk/lib-storage/@smithy/types": ["@smithy/types@4.14.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg=="], - - "@aws-sdk/lib-storage/buffer": ["buffer@5.6.0", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" } }, "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw=="], - "@aws-sdk/middleware-flexible-checksums/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], "@aws-sdk/middleware-sdk-s3/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], @@ -5483,8 +5398,6 @@ "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], - "@daytona/sdk/@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HSRBzXHIC7C8UfPQdu15zEEoBGv0yWkhEwxqgPCHVUKUQ9NLHVGXkVrf65Uaj7UwmAkC1gQfkuVYvLlD//AnUQ=="], - "@develar/schema-utils/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -5529,8 +5442,6 @@ "@gitlab/opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - "@grpc/proto-loader/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], "@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -5671,106 +5582,16 @@ "@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], - "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], - - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], - - "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], - - "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - - "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - - "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - - "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - - "@opentelemetry/exporter-prometheus/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/exporter-prometheus/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - - "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - - "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="], - "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - - "@opentelemetry/exporter-zipkin/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/exporter-zipkin/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - - "@opentelemetry/instrumentation-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/otlp-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="], - "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - "@opentelemetry/propagator-b3/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/propagator-jaeger/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - "@opentelemetry/sdk-logs/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="], - "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - "@opentelemetry/sdk-node/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HSRBzXHIC7C8UfPQdu15zEEoBGv0yWkhEwxqgPCHVUKUQ9NLHVGXkVrf65Uaj7UwmAkC1gQfkuVYvLlD//AnUQ=="], - - "@opentelemetry/sdk-node/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], - - "@opentelemetry/sdk-node/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - - "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - - "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.2.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.2.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-+OaRja3f0IqGG2kptVeYsrZQK9nKRSpfFrKtRBq4uh6nIB8bTBgaGvYQrQoRrQWQMA5dK5yLhDMDc0dvYvCOIQ=="], - "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], "@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], @@ -5901,8 +5722,6 @@ "app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], - "app-builder-lib/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "app-builder-lib/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], "app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], @@ -5949,6 +5768,8 @@ "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + "c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], + "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], "compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], @@ -5979,8 +5800,6 @@ "dot-prop/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="], - "dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "editorconfig/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], "editorconfig/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -6215,8 +6034,6 @@ "storybook-solidjs-vite/vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="], - "stream-browserify/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -6225,6 +6042,8 @@ "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], @@ -6481,26 +6300,6 @@ "@aws-sdk/credential-providers/@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core": ["@smithy/core@3.23.15", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-stream": "^4.5.23", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-E7GVCgsQttzfujEZb6Qep005wWf4xiL4x06apFEtzQMWYBPggZh/0cnOxPficw5cuK/YjjkehKoIN4YUaSh0UQ=="], - - "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.18", "", { "dependencies": { "@smithy/core": "^3.23.15", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-M6CSgnp3v4tYz9ynj2JHbA60woBZcGqEwNjTKjBsNHPV26R1ZX52+0wW8WsZU18q45jD0tw2wL22S17Ze9LpEw=="], - - "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.14", "", { "dependencies": { "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg=="], - - "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.9", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ=="], - - "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.14", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ=="], - - "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/util-middleware": ["@smithy/util-middleware@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw=="], - - "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core": ["@smithy/core@3.23.15", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-stream": "^4.5.23", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-E7GVCgsQttzfujEZb6Qep005wWf4xiL4x06apFEtzQMWYBPggZh/0cnOxPficw5cuK/YjjkehKoIN4YUaSh0UQ=="], - - "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA=="], - - "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream": ["@smithy/util-stream@4.5.23", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.5.3", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-N6on1+ngJ3RznZOnDWNveIwnTSlqxNnXuNAh7ez889ZZaRdXoNRTXKgmYOLe6dB0gCmAVtuRScE1hymQFl4hpg=="], - - "@aws-sdk/lib-storage/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.17", "", { "dependencies": { "@smithy/types": "^4.14.0", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg=="], "@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-endpoints": "^3.3.4", "tslib": "^2.6.2" } }, "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg=="], @@ -6537,12 +6336,6 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "@daytona/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@daytona/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@daytona/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - "@develar/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], @@ -6585,10 +6378,6 @@ "@gitlab/opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - "@grpc/proto-loader/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "@grpc/proto-loader/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@jsx-email/cli/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], "@jsx-email/cli/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], @@ -6767,52 +6556,6 @@ "@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], - "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - - "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - - "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - - "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], - - "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - - "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], - - "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - - "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], - - "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - - "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], - - "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - - "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], - - "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - - "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], - - "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - - "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - - "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], - - "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - - "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - - "@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], - - "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.2.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ=="], - "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], @@ -6937,10 +6680,6 @@ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - - "minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "motion/framer-motion/motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="], "motion/framer-motion/motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="], @@ -6963,8 +6702,6 @@ "opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], - "openid-client/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "ora/bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "ora/bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -7125,34 +6862,10 @@ "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], - "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.23", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.5.3", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-N6on1+ngJ3RznZOnDWNveIwnTSlqxNnXuNAh7ez889ZZaRdXoNRTXKgmYOLe6dB0gCmAVtuRScE1hymQFl4hpg=="], - - "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ=="], - - "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw=="], - - "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/url-parser": ["@smithy/url-parser@4.2.14", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ=="], - - "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw=="], - - "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - - "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.17", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw=="], - - "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.3", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-lc5jFL++x17sPhIwMWJ3YOnqmSjw/2Po6VLDlUIXvxVWRuJwRXnJ4jOBBLB0cfI5BB5ehIl02Fxr1PDvk/kxDw=="], - - "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], - "@daytona/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], - - "@daytona/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - "@electron/asar/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "@electron/rebuild/node-gyp/make-fetch-happen/@npmcli/agent": ["@npmcli/agent@3.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q=="], @@ -7177,14 +6890,6 @@ "@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "@grpc/proto-loader/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@grpc/proto-loader/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "@grpc/proto-loader/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@grpc/proto-loader/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@jsx-email/cli/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@jsx-email/cli/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -7271,8 +6976,6 @@ "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], - "app-builder-lib/hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "archiver-utils/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], "archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], @@ -7389,16 +7092,6 @@ "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], - "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.17", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw=="], - - "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.3", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-lc5jFL++x17sPhIwMWJ3YOnqmSjw/2Po6VLDlUIXvxVWRuJwRXnJ4jOBBLB0cfI5BB5ehIl02Fxr1PDvk/kxDw=="], - - "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw=="], - - "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A=="], - - "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A=="], - "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], @@ -7417,10 +7110,6 @@ "@electron/rebuild/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@grpc/proto-loader/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@grpc/proto-loader/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="], @@ -7459,10 +7148,6 @@ "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], - "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A=="], - - "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A=="], - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -7483,8 +7168,6 @@ "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], - "@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch/minipass-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], diff --git a/daytona.ts b/daytona.ts deleted file mode 100644 index 4007b77aa6..0000000000 --- a/daytona.ts +++ /dev/null @@ -1,197 +0,0 @@ -import type { Daytona, Sandbox } from "@daytonaio/sdk" -import type { Plugin } from "@opencode-ai/plugin" -import { join } from "node:path" -import { fileURLToPath } from "node:url" -import { tmpdir } from "node:os" -import { access, copyFile, mkdir } from "node:fs/promises" - -let client: Promise | undefined - -let daytona = function daytona(): Promise { - if (client == null) { - client = import("@daytonaio/sdk").then( - ({ Daytona }) => - new Daytona({ - apiKey: "dtn_d63c206564ef49d4104ec2cd755e561bb3665beed8fd7d7ab2c5f7a2186965f0", - }), - ) - } - return client -} - -const preview = new Map() -const repo = "/home/daytona/workspace/repo" -const root = "/home/daytona/workspace" -const localbin = "/home/daytona/opencode" -const installbin = "/home/daytona/.opencode/bin/opencode" -const health = "http://127.0.0.1:3096/global/health" - -const local = fileURLToPath( - new URL("./packages/opencode/dist/opencode-linux-x64-baseline/bin/opencode", import.meta.url), -) - -async function exists(file: string) { - return access(file) - .then(() => true) - .catch(() => false) -} - -function sh(value: string) { - return `'${value.replace(/'/g, `"'"'"`)}'` -} - -// Internally Daytona uses axios, which tries to overwrite stack -// traces when a failure happens. That path fails in Bun, however, so -// when something goes wrong you only see a very obscure error. -async function withSandbox(name: string, fn: (sandbox: Sandbox) => Promise) { - const stack = Error.captureStackTrace - // @ts-expect-error temporary compatibility hack for Daytona's axios stack handling in Bun - Error.captureStackTrace = undefined - try { - return await fn(await (await daytona()).get(name)) - } finally { - Error.captureStackTrace = stack - } -} - -export const DaytonaWorkspacePlugin: Plugin = async ({ experimental_workspace, worktree, project }) => { - experimental_workspace.register("daytona", { - name: "Daytona", - description: "Create a remote Daytona workspace", - configure(config) { - return config - }, - async create(config, env) { - const temp = join(tmpdir(), `opencode-daytona-${Date.now()}`) - - console.log("creating sandbox...") - - const sandbox = await ( - await daytona() - ).create({ - name: config.name, - snapshot: "daytona-large", - envVars: env, - }) - - console.log("creating ssh...") - - const ssh = await withSandbox(config.name, (sandbox) => sandbox.createSshAccess()) - console.log("daytona:", ssh.sshCommand) - - const run = async (command: string) => { - console.log("sandbox:", command) - const result = await sandbox.process.executeCommand(command) - if (result.result) process.stdout.write(result.result) - if (result.exitCode === 0) return result - throw new Error(result.result || `sandbox command failed: ${command}`) - } - - const wait = async () => { - for (let i = 0; i < 60; i++) { - const result = await sandbox.process.executeCommand(`curl -fsS ${sh(health)}`) - if (result.exitCode === 0) { - if (result.result) process.stdout.write(result.result) - return - } - console.log(`waiting for server (${i + 1}/60)`) - await Bun.sleep(1000) - } - - const log = await sandbox.process.executeCommand(`test -f /tmp/opencode.log && cat /tmp/opencode.log || true`) - throw new Error(log.result || "daytona workspace server did not become ready in time") - } - - const dir = join(temp, "repo") - const tar = join(temp, "repo.tgz") - const source = `file://${worktree}` - await mkdir(temp, { recursive: true }) - const args = ["clone", "--depth", "1", "--no-local"] - if (config.branch) args.push("--branch", config.branch) - args.push(source, dir) - - console.log("git cloning...") - - const clone = Bun.spawn(["git", ...args], { - cwd: tmpdir(), - stdout: "pipe", - stderr: "pipe", - }) - const code = await clone.exited - if (code !== 0) throw new Error(await new Response(clone.stderr).text()) - - const configPackage = join(worktree, ".opencode", "package.json") - // if (await exists(configPackage)) { - // console.log("copying config package...") - // await mkdir(join(dir, ".opencode"), { recursive: true }) - // await copyFile(configPackage, join(dir, ".opencode", "package.json")) - // } - - console.log("tarring...") - - const packed = Bun.spawn(["tar", "-czf", tar, "-C", temp, "repo"], { - stdout: "ignore", - stderr: "pipe", - }) - if ((await packed.exited) !== 0) throw new Error(await new Response(packed.stderr).text()) - - console.log("uploading files...") - - await sandbox.fs.uploadFile(tar, "repo.tgz") - - const have = await exists(local) - console.log("local", local) - if (have) { - console.log("uploading local binary...") - await sandbox.fs.uploadFile(local, "opencode") - } - - console.log("bootstrapping workspace...") - await run(`rm -rf ${sh(repo)} && mkdir -p ${sh(root)} && tar -xzf \"$HOME/repo.tgz\" -C \"$HOME/workspace\"`) - - if (have) { - await run(`chmod +x ${sh(localbin)}`) - } else { - await run( - `mkdir -p \"$HOME/.opencode/bin\" && OPENCODE_INSTALL_DIR=\"$HOME/.opencode/bin\" curl -fsSL https://opencode.ai/install | bash`, - ) - } - - await run(`printf \"%s\\n\" ${sh(project.id)} > ${sh(`${repo}/.git/opencode`)}`) - - console.log("starting server...") - await run( - `cd ${sh(repo)} && exe=${sh(localbin)} && if [ ! -x \"$exe\" ]; then exe=${sh(installbin)}; fi && nohup env \"$exe\" serve --hostname 0.0.0.0 --port 3096 >/tmp/opencode.log 2>&1 undefined) - if (!sandbox) return - await (await daytona()).delete(sandbox) - preview.delete(config.name) - }, - async target(config) { - let link = preview.get(config.name) - if (!link) { - link = await withSandbox(config.name, (sandbox) => sandbox.getPreviewLink(3096)) - preview.set(config.name, link) - } - return { - type: "remote", - url: link.url, - headers: { - "x-daytona-preview-token": link.token, - "x-daytona-skip-preview-warning": "true", - "x-opencode-directory": repo, - }, - } - }, - }) - - return {} -} - -export default DaytonaWorkspacePlugin diff --git a/package.json b/package.json index 09548bf4e0..5fecc09922 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,6 @@ }, "dependencies": { "@aws-sdk/client-s3": "3.933.0", - "@daytona/sdk": "0.167.0", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", From e7f8f7fa3bceab49b1606de72d969be63c3e8785 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 17 Apr 2026 01:14:08 -0400 Subject: [PATCH 073/335] fix crash on experimental --- packages/opencode/src/flag/flag.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index a1c46288a2..72c8931f5b 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -81,7 +81,7 @@ export const Flag = { OPENCODE_STRICT_CONFIG_DEPS: truthy("OPENCODE_STRICT_CONFIG_DEPS"), OPENCODE_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"], - OPENCODE_EXPERIMENTAL_HTTPAPI: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_HTTPAPI"), + OPENCODE_EXPERIMENTAL_HTTPAPI: truthy("OPENCODE_EXPERIMENTAL_HTTPAPI"), OPENCODE_EXPERIMENTAL_WORKSPACES: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WORKSPACES"), // Evaluated at access time (not module load) because tests, the CLI, and From 7605acff650db0d41d80429b662b5c0725d89675 Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 17 Apr 2026 02:06:20 -0400 Subject: [PATCH 074/335] refactor(core): move server routes around to clarify workspacing (#23031) --- .../src/control-plane/workspace-context.ts | 4 +- .../src/server/{ => routes}/control/index.ts | 6 +- .../{instance => routes/control}/workspace.ts | 10 +- .../src/server/{instance => routes}/global.ts | 0 .../server/{ => routes}/instance/config.ts | 10 +- .../src/server/{ => routes}/instance/event.ts | 2 +- .../{ => routes}/instance/experimental.ts | 30 +- .../src/server/{ => routes}/instance/file.ts | 12 +- .../{ => routes}/instance/httpapi/config.ts | 0 .../instance/httpapi/permission.ts | 0 .../{ => routes}/instance/httpapi/project.ts | 0 .../{ => routes}/instance/httpapi/provider.ts | 0 .../{ => routes}/instance/httpapi/question.ts | 0 .../{ => routes}/instance/httpapi/server.ts | 0 .../src/server/{ => routes}/instance/index.ts | 19 +- .../src/server/{ => routes}/instance/mcp.ts | 12 +- .../{ => routes}/instance/permission.ts | 4 +- .../server/{ => routes}/instance/project.ts | 12 +- .../server/{ => routes}/instance/provider.ts | 16 +- .../src/server/{ => routes}/instance/pty.ts | 4 +- .../server/{ => routes}/instance/question.ts | 6 +- .../server/{ => routes}/instance/session.ts | 26 +- .../src/server/{ => routes}/instance/sync.ts | 2 +- .../src/server/{ => routes}/instance/trace.ts | 2 +- .../src/server/{ => routes}/instance/tui.ts | 10 +- .../src/server/{ui/index.ts => routes/ui.ts} | 0 packages/opencode/src/server/server.ts | 72 +- .../{instance/middleware.ts => workspace.ts} | 43 +- .../test/server/session-messages.test.ts | 13 - packages/sdk/js/src/v2/gen/sdk.gen.ts | 858 ++++++++--------- packages/sdk/js/src/v2/gen/types.gen.ts | 362 ++++---- packages/sdk/openapi.json | 870 +++++++++--------- 32 files changed, 1196 insertions(+), 1209 deletions(-) rename packages/opencode/src/server/{ => routes}/control/index.ts (96%) rename packages/opencode/src/server/{instance => routes/control}/workspace.ts (95%) rename packages/opencode/src/server/{instance => routes}/global.ts (100%) rename packages/opencode/src/server/{ => routes}/instance/config.ts (92%) rename packages/opencode/src/server/{ => routes}/instance/event.ts (97%) rename packages/opencode/src/server/{ => routes}/instance/experimental.ts (94%) rename packages/opencode/src/server/{ => routes}/instance/file.ts (95%) rename packages/opencode/src/server/{ => routes}/instance/httpapi/config.ts (100%) rename packages/opencode/src/server/{ => routes}/instance/httpapi/permission.ts (100%) rename packages/opencode/src/server/{ => routes}/instance/httpapi/project.ts (100%) rename packages/opencode/src/server/{ => routes}/instance/httpapi/provider.ts (100%) rename packages/opencode/src/server/{ => routes}/instance/httpapi/question.ts (100%) rename packages/opencode/src/server/{ => routes}/instance/httpapi/server.ts (100%) rename packages/opencode/src/server/{ => routes}/instance/index.ts (95%) rename packages/opencode/src/server/{ => routes}/instance/mcp.ts (96%) rename packages/opencode/src/server/{ => routes}/instance/permission.ts (96%) rename packages/opencode/src/server/{ => routes}/instance/project.ts (92%) rename packages/opencode/src/server/{ => routes}/instance/provider.ts (93%) rename packages/opencode/src/server/{ => routes}/instance/pty.ts (98%) rename packages/opencode/src/server/{ => routes}/instance/question.ts (96%) rename packages/opencode/src/server/{ => routes}/instance/session.ts (98%) rename packages/opencode/src/server/{ => routes}/instance/sync.ts (98%) rename packages/opencode/src/server/{ => routes}/instance/trace.ts (93%) rename packages/opencode/src/server/{ => routes}/instance/tui.ts (98%) rename packages/opencode/src/server/{ui/index.ts => routes/ui.ts} (100%) rename packages/opencode/src/server/{instance/middleware.ts => workspace.ts} (79%) diff --git a/packages/opencode/src/control-plane/workspace-context.ts b/packages/opencode/src/control-plane/workspace-context.ts index 3d4fa5baef..85ef596e7a 100644 --- a/packages/opencode/src/control-plane/workspace-context.ts +++ b/packages/opencode/src/control-plane/workspace-context.ts @@ -2,13 +2,13 @@ import { LocalContext } from "../util" import type { WorkspaceID } from "../control-plane/schema" export interface WorkspaceContext { - workspaceID: WorkspaceID + workspaceID: WorkspaceID | undefined } const context = LocalContext.create("instance") export const WorkspaceContext = { - async provide(input: { workspaceID: WorkspaceID; fn: () => R }): Promise { + async provide(input: { workspaceID?: WorkspaceID; fn: () => R }): Promise { return context.provide({ workspaceID: input.workspaceID }, () => input.fn()) }, diff --git a/packages/opencode/src/server/control/index.ts b/packages/opencode/src/server/routes/control/index.ts similarity index 96% rename from packages/opencode/src/server/control/index.ts rename to packages/opencode/src/server/routes/control/index.ts index 737f958d6b..3fd60636ff 100644 --- a/packages/opencode/src/server/control/index.ts +++ b/packages/opencode/src/server/routes/control/index.ts @@ -6,13 +6,12 @@ import { ProviderID } from "@/provider/schema" import { Hono } from "hono" import { describeRoute, resolver, validator, openAPIRouteHandler } from "hono-openapi" import z from "zod" -import { errors } from "../error" -import { GlobalRoutes } from "../instance/global" +import { errors } from "../../error" +import { WorkspaceRoutes } from "./workspace" export function ControlPlaneRoutes(): Hono { const app = new Hono() return app - .route("/global", GlobalRoutes()) .put( "/auth/:providerID", describeRoute({ @@ -159,4 +158,5 @@ export function ControlPlaneRoutes(): Hono { return c.json(true) }, ) + .route("/experimental/workspace", WorkspaceRoutes()) } diff --git a/packages/opencode/src/server/instance/workspace.ts b/packages/opencode/src/server/routes/control/workspace.ts similarity index 95% rename from packages/opencode/src/server/instance/workspace.ts rename to packages/opencode/src/server/routes/control/workspace.ts index 59369ef8e7..9ff747b68a 100644 --- a/packages/opencode/src/server/instance/workspace.ts +++ b/packages/opencode/src/server/routes/control/workspace.ts @@ -1,11 +1,11 @@ import { Hono } from "hono" import { describeRoute, resolver, validator } from "hono-openapi" import z from "zod" -import { listAdaptors } from "../../control-plane/adaptors" -import { Workspace } from "../../control-plane/workspace" -import { Instance } from "../../project/instance" -import { errors } from "../error" -import { lazy } from "../../util/lazy" +import { listAdaptors } from "@/control-plane/adaptors" +import { Workspace } from "@/control-plane/workspace" +import { Instance } from "@/project/instance" +import { errors } from "../../error" +import { lazy } from "@/util/lazy" import { Log } from "@/util" import { errorData } from "@/util/error" diff --git a/packages/opencode/src/server/instance/global.ts b/packages/opencode/src/server/routes/global.ts similarity index 100% rename from packages/opencode/src/server/instance/global.ts rename to packages/opencode/src/server/routes/global.ts diff --git a/packages/opencode/src/server/instance/config.ts b/packages/opencode/src/server/routes/instance/config.ts similarity index 92% rename from packages/opencode/src/server/instance/config.ts rename to packages/opencode/src/server/routes/instance/config.ts index 15c393fe5a..235f5682e2 100644 --- a/packages/opencode/src/server/instance/config.ts +++ b/packages/opencode/src/server/routes/instance/config.ts @@ -1,11 +1,11 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" -import { Config } from "../../config" -import { Provider } from "../../provider" -import { errors } from "../error" -import { lazy } from "../../util/lazy" -import { AppRuntime } from "../../effect/app-runtime" +import { Config } from "@/config" +import { Provider } from "@/provider" +import { errors } from "../../error" +import { lazy } from "@/util/lazy" +import { AppRuntime } from "@/effect/app-runtime" import { jsonRequest } from "./trace" export const ConfigRoutes = lazy(() => diff --git a/packages/opencode/src/server/instance/event.ts b/packages/opencode/src/server/routes/instance/event.ts similarity index 97% rename from packages/opencode/src/server/instance/event.ts rename to packages/opencode/src/server/routes/instance/event.ts index 103d3d7cfb..1d883bd883 100644 --- a/packages/opencode/src/server/instance/event.ts +++ b/packages/opencode/src/server/routes/instance/event.ts @@ -5,7 +5,7 @@ import { streamSSE } from "hono/streaming" import { Log } from "@/util" import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" -import { AsyncQueue } from "../../util/queue" +import { AsyncQueue } from "@/util/queue" const log = Log.create({ service: "server" }) diff --git a/packages/opencode/src/server/instance/experimental.ts b/packages/opencode/src/server/routes/instance/experimental.ts similarity index 94% rename from packages/opencode/src/server/instance/experimental.ts rename to packages/opencode/src/server/routes/instance/experimental.ts index 6fe99a8c3b..f7ecc8255b 100644 --- a/packages/opencode/src/server/instance/experimental.ts +++ b/packages/opencode/src/server/routes/instance/experimental.ts @@ -1,22 +1,21 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" -import { ProviderID, ModelID } from "../../provider/schema" -import { ToolRegistry } from "../../tool" -import { Worktree } from "../../worktree" -import { Instance } from "../../project/instance" -import { Project } from "../../project" -import { MCP } from "../../mcp" -import { Session } from "../../session" -import { Config } from "../../config" -import { ConsoleState } from "../../config/console-state" -import { Account } from "../../account/account" -import { AccountID, OrgID } from "../../account/schema" -import { AppRuntime } from "../../effect/app-runtime" -import { errors } from "../error" -import { lazy } from "../../util/lazy" +import { ProviderID, ModelID } from "@/provider/schema" +import { ToolRegistry } from "@/tool" +import { Worktree } from "@/worktree" +import { Instance } from "@/project/instance" +import { Project } from "@/project" +import { MCP } from "@/mcp" +import { Session } from "@/session" +import { Config } from "@/config" +import { ConsoleState } from "@/config/console-state" +import { Account } from "@/account/account" +import { AccountID, OrgID } from "@/account/schema" +import { AppRuntime } from "@/effect/app-runtime" +import { errors } from "../../error" +import { lazy } from "@/util/lazy" import { Effect, Option } from "effect" -import { WorkspaceRoutes } from "./workspace" import { Agent } from "@/agent/agent" const ConsoleOrgOption = z.object({ @@ -231,7 +230,6 @@ export const ExperimentalRoutes = lazy(() => ) }, ) - .route("/workspace", WorkspaceRoutes()) .post( "/worktree", describeRoute({ diff --git a/packages/opencode/src/server/instance/file.ts b/packages/opencode/src/server/routes/instance/file.ts similarity index 95% rename from packages/opencode/src/server/instance/file.ts rename to packages/opencode/src/server/routes/instance/file.ts index db5e227770..a82e5687d8 100644 --- a/packages/opencode/src/server/instance/file.ts +++ b/packages/opencode/src/server/routes/instance/file.ts @@ -2,12 +2,12 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import { Effect } from "effect" import z from "zod" -import { AppRuntime } from "../../effect/app-runtime" -import { File } from "../../file" -import { Ripgrep } from "../../file/ripgrep" -import { LSP } from "../../lsp" -import { Instance } from "../../project/instance" -import { lazy } from "../../util/lazy" +import { AppRuntime } from "@/effect/app-runtime" +import { File } from "@/file" +import { Ripgrep } from "@/file/ripgrep" +import { LSP } from "@/lsp" +import { Instance } from "@/project/instance" +import { lazy } from "@/util/lazy" export const FileRoutes = lazy(() => new Hono() diff --git a/packages/opencode/src/server/instance/httpapi/config.ts b/packages/opencode/src/server/routes/instance/httpapi/config.ts similarity index 100% rename from packages/opencode/src/server/instance/httpapi/config.ts rename to packages/opencode/src/server/routes/instance/httpapi/config.ts diff --git a/packages/opencode/src/server/instance/httpapi/permission.ts b/packages/opencode/src/server/routes/instance/httpapi/permission.ts similarity index 100% rename from packages/opencode/src/server/instance/httpapi/permission.ts rename to packages/opencode/src/server/routes/instance/httpapi/permission.ts diff --git a/packages/opencode/src/server/instance/httpapi/project.ts b/packages/opencode/src/server/routes/instance/httpapi/project.ts similarity index 100% rename from packages/opencode/src/server/instance/httpapi/project.ts rename to packages/opencode/src/server/routes/instance/httpapi/project.ts diff --git a/packages/opencode/src/server/instance/httpapi/provider.ts b/packages/opencode/src/server/routes/instance/httpapi/provider.ts similarity index 100% rename from packages/opencode/src/server/instance/httpapi/provider.ts rename to packages/opencode/src/server/routes/instance/httpapi/provider.ts diff --git a/packages/opencode/src/server/instance/httpapi/question.ts b/packages/opencode/src/server/routes/instance/httpapi/question.ts similarity index 100% rename from packages/opencode/src/server/instance/httpapi/question.ts rename to packages/opencode/src/server/routes/instance/httpapi/question.ts diff --git a/packages/opencode/src/server/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts similarity index 100% rename from packages/opencode/src/server/instance/httpapi/server.ts rename to packages/opencode/src/server/routes/instance/httpapi/server.ts diff --git a/packages/opencode/src/server/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts similarity index 95% rename from packages/opencode/src/server/instance/index.ts rename to packages/opencode/src/server/routes/instance/index.ts index cfcaffc596..017541b8fc 100644 --- a/packages/opencode/src/server/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -3,15 +3,15 @@ import { Hono } from "hono" import type { UpgradeWebSocket } from "hono/ws" import { Context, Effect } from "effect" import z from "zod" -import { Format } from "../../format" +import { Format } from "@/format" import { TuiRoutes } from "./tui" -import { Instance } from "../../project/instance" -import { Vcs } from "../../project" -import { Agent } from "../../agent/agent" -import { Skill } from "../../skill" -import { Global } from "../../global" -import { LSP } from "../../lsp" -import { Command } from "../../command" +import { Instance } from "@/project/instance" +import { Vcs } from "@/project" +import { Agent } from "@/agent/agent" +import { Skill } from "@/skill" +import { Global } from "@/global" +import { LSP } from "@/lsp" +import { Command } from "@/command" import { QuestionRoutes } from "./question" import { PermissionRoutes } from "./permission" import { Flag } from "@/flag/flag" @@ -26,11 +26,10 @@ import { ExperimentalRoutes } from "./experimental" import { ProviderRoutes } from "./provider" import { EventRoutes } from "./event" import { SyncRoutes } from "./sync" -import { WorkspaceRouterMiddleware } from "./middleware" import { AppRuntime } from "@/effect/app-runtime" export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { - const app = new Hono().use(WorkspaceRouterMiddleware(upgrade)) + const app = new Hono() if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) { const handler = ExperimentalHttpApiServer.webHandler().handler diff --git a/packages/opencode/src/server/instance/mcp.ts b/packages/opencode/src/server/routes/instance/mcp.ts similarity index 96% rename from packages/opencode/src/server/instance/mcp.ts rename to packages/opencode/src/server/routes/instance/mcp.ts index f6e6f1eddb..197185bde0 100644 --- a/packages/opencode/src/server/instance/mcp.ts +++ b/packages/opencode/src/server/routes/instance/mcp.ts @@ -1,12 +1,12 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" -import { MCP } from "../../mcp" -import { Config } from "../../config" -import { ConfigMCP } from "../../config/mcp" -import { AppRuntime } from "../../effect/app-runtime" -import { errors } from "../error" -import { lazy } from "../../util/lazy" +import { MCP } from "@/mcp" +import { Config } from "@/config" +import { ConfigMCP } from "@/config/mcp" +import { AppRuntime } from "@/effect/app-runtime" +import { errors } from "../../error" +import { lazy } from "@/util/lazy" import { Effect } from "effect" export const McpRoutes = lazy(() => diff --git a/packages/opencode/src/server/instance/permission.ts b/packages/opencode/src/server/routes/instance/permission.ts similarity index 96% rename from packages/opencode/src/server/instance/permission.ts rename to packages/opencode/src/server/routes/instance/permission.ts index b8c2244140..c3f9c82011 100644 --- a/packages/opencode/src/server/instance/permission.ts +++ b/packages/opencode/src/server/routes/instance/permission.ts @@ -4,8 +4,8 @@ import z from "zod" import { AppRuntime } from "@/effect/app-runtime" import { Permission } from "@/permission" import { PermissionID } from "@/permission/schema" -import { errors } from "../error" -import { lazy } from "../../util/lazy" +import { errors } from "../../error" +import { lazy } from "@/util/lazy" export const PermissionRoutes = lazy(() => new Hono() diff --git a/packages/opencode/src/server/instance/project.ts b/packages/opencode/src/server/routes/instance/project.ts similarity index 92% rename from packages/opencode/src/server/instance/project.ts rename to packages/opencode/src/server/routes/instance/project.ts index 95b5862fd5..060542c4b4 100644 --- a/packages/opencode/src/server/instance/project.ts +++ b/packages/opencode/src/server/routes/instance/project.ts @@ -1,13 +1,13 @@ import { Hono } from "hono" import { describeRoute, validator } from "hono-openapi" import { resolver } from "hono-openapi" -import { Instance } from "../../project/instance" -import { Project } from "../../project" +import { Instance } from "@/project/instance" +import { Project } from "@/project" import z from "zod" -import { ProjectID } from "../../project/schema" -import { errors } from "../error" -import { lazy } from "../../util/lazy" -import { InstanceBootstrap } from "../../project/bootstrap" +import { ProjectID } from "@/project/schema" +import { errors } from "../../error" +import { lazy } from "@/util/lazy" +import { InstanceBootstrap } from "@/project/bootstrap" import { AppRuntime } from "@/effect/app-runtime" export const ProjectRoutes = lazy(() => diff --git a/packages/opencode/src/server/instance/provider.ts b/packages/opencode/src/server/routes/instance/provider.ts similarity index 93% rename from packages/opencode/src/server/instance/provider.ts rename to packages/opencode/src/server/routes/instance/provider.ts index a81ae00d59..57aa895e3d 100644 --- a/packages/opencode/src/server/instance/provider.ts +++ b/packages/opencode/src/server/routes/instance/provider.ts @@ -1,15 +1,15 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" -import { Config } from "../../config" -import { Provider } from "../../provider" -import { ModelsDev } from "../../provider" -import { ProviderAuth } from "../../provider" -import { ProviderID } from "../../provider/schema" -import { AppRuntime } from "../../effect/app-runtime" +import { Config } from "@/config" +import { Provider } from "@/provider" +import { ModelsDev } from "@/provider" +import { ProviderAuth } from "@/provider" +import { ProviderID } from "@/provider/schema" +import { AppRuntime } from "@/effect/app-runtime" import { mapValues } from "remeda" -import { errors } from "../error" -import { lazy } from "../../util/lazy" +import { errors } from "../../error" +import { lazy } from "@/util/lazy" import { Effect } from "effect" export const ProviderRoutes = lazy(() => diff --git a/packages/opencode/src/server/instance/pty.ts b/packages/opencode/src/server/routes/instance/pty.ts similarity index 98% rename from packages/opencode/src/server/instance/pty.ts rename to packages/opencode/src/server/routes/instance/pty.ts index 7943725120..b3f71c235c 100644 --- a/packages/opencode/src/server/instance/pty.ts +++ b/packages/opencode/src/server/routes/instance/pty.ts @@ -6,8 +6,8 @@ import z from "zod" import { AppRuntime } from "@/effect/app-runtime" import { Pty } from "@/pty" import { PtyID } from "@/pty/schema" -import { NotFoundError } from "../../storage" -import { errors } from "../error" +import { NotFoundError } from "@/storage" +import { errors } from "../../error" export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) { return new Hono() diff --git a/packages/opencode/src/server/instance/question.ts b/packages/opencode/src/server/routes/instance/question.ts similarity index 96% rename from packages/opencode/src/server/instance/question.ts rename to packages/opencode/src/server/routes/instance/question.ts index 0f61a18672..9b8f461e39 100644 --- a/packages/opencode/src/server/instance/question.ts +++ b/packages/opencode/src/server/routes/instance/question.ts @@ -2,11 +2,11 @@ import { Hono } from "hono" import { describeRoute, validator } from "hono-openapi" import { resolver } from "hono-openapi" import { QuestionID } from "@/question/schema" -import { Question } from "../../question" +import { Question } from "@/question" import { AppRuntime } from "@/effect/app-runtime" import z from "zod" -import { errors } from "../error" -import { lazy } from "../../util/lazy" +import { errors } from "../../error" +import { lazy } from "@/util/lazy" const Reply = z.object({ answers: Question.Answer.zod diff --git a/packages/opencode/src/server/instance/session.ts b/packages/opencode/src/server/routes/instance/session.ts similarity index 98% rename from packages/opencode/src/server/instance/session.ts rename to packages/opencode/src/server/routes/instance/session.ts index 1511e99e8d..ae6185abb8 100644 --- a/packages/opencode/src/server/instance/session.ts +++ b/packages/opencode/src/server/routes/instance/session.ts @@ -3,28 +3,28 @@ import { stream } from "hono/streaming" import { describeRoute, validator, resolver } from "hono-openapi" import { SessionID, MessageID, PartID } from "@/session/schema" import z from "zod" -import { Session } from "../../session" -import { MessageV2 } from "../../session/message-v2" -import { SessionPrompt } from "../../session/prompt" +import { Session } from "@/session" +import { MessageV2 } from "@/session/message-v2" +import { SessionPrompt } from "@/session/prompt" import { SessionRunState } from "@/session/run-state" -import { SessionCompaction } from "../../session/compaction" -import { SessionRevert } from "../../session/revert" +import { SessionCompaction } from "@/session/compaction" +import { SessionRevert } from "@/session/revert" import { SessionShare } from "@/share" import { SessionStatus } from "@/session/status" import { SessionSummary } from "@/session/summary" -import { Todo } from "../../session/todo" +import { Todo } from "@/session/todo" import { Effect } from "effect" -import { AppRuntime } from "../../effect/app-runtime" -import { Agent } from "../../agent/agent" +import { AppRuntime } from "@/effect/app-runtime" +import { Agent } from "@/agent/agent" import { Snapshot } from "@/snapshot" -import { Command } from "../../command" -import { Log } from "../../util" +import { Command } from "@/command" +import { Log } from "@/util" import { Permission } from "@/permission" import { PermissionID } from "@/permission/schema" import { ModelID, ProviderID } from "@/provider/schema" -import { errors } from "../error" -import { lazy } from "../../util/lazy" -import { Bus } from "../../bus" +import { errors } from "../../error" +import { lazy } from "@/util/lazy" +import { Bus } from "@/bus" import { NamedError } from "@opencode-ai/shared/util/error" import { jsonRequest } from "./trace" diff --git a/packages/opencode/src/server/instance/sync.ts b/packages/opencode/src/server/routes/instance/sync.ts similarity index 98% rename from packages/opencode/src/server/instance/sync.ts rename to packages/opencode/src/server/routes/instance/sync.ts index ac43b638eb..c6a067997b 100644 --- a/packages/opencode/src/server/instance/sync.ts +++ b/packages/opencode/src/server/routes/instance/sync.ts @@ -6,7 +6,7 @@ import { Database, asc, and, not, or, lte, eq } from "@/storage" import { EventTable } from "@/sync/event.sql" import { lazy } from "@/util/lazy" import { Log } from "@/util" -import { errors } from "../error" +import { errors } from "../../error" const ReplayEvent = z.object({ id: z.string(), diff --git a/packages/opencode/src/server/instance/trace.ts b/packages/opencode/src/server/routes/instance/trace.ts similarity index 93% rename from packages/opencode/src/server/instance/trace.ts rename to packages/opencode/src/server/routes/instance/trace.ts index b3adbb4c80..3e1f72d8b2 100644 --- a/packages/opencode/src/server/instance/trace.ts +++ b/packages/opencode/src/server/routes/instance/trace.ts @@ -1,6 +1,6 @@ import type { Context } from "hono" import { Effect } from "effect" -import { AppRuntime } from "../../effect/app-runtime" +import { AppRuntime } from "@/effect/app-runtime" type AppEnv = Parameters[0] extends Effect.Effect ? R : never diff --git a/packages/opencode/src/server/instance/tui.ts b/packages/opencode/src/server/routes/instance/tui.ts similarity index 98% rename from packages/opencode/src/server/instance/tui.ts rename to packages/opencode/src/server/routes/instance/tui.ts index 0073ef98c9..2f856c3488 100644 --- a/packages/opencode/src/server/instance/tui.ts +++ b/packages/opencode/src/server/routes/instance/tui.ts @@ -1,13 +1,13 @@ import { Hono, type Context } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" -import { Bus } from "../../bus" -import { Session } from "../../session" +import { Bus } from "@/bus" +import { Session } from "@/session" import { TuiEvent } from "@/cli/cmd/tui/event" import { AppRuntime } from "@/effect/app-runtime" -import { AsyncQueue } from "../../util/queue" -import { errors } from "../error" -import { lazy } from "../../util/lazy" +import { AsyncQueue } from "@/util/queue" +import { errors } from "../../error" +import { lazy } from "@/util/lazy" const TuiRequest = z.object({ path: z.string(), diff --git a/packages/opencode/src/server/ui/index.ts b/packages/opencode/src/server/routes/ui.ts similarity index 100% rename from packages/opencode/src/server/ui/index.ts rename to packages/opencode/src/server/routes/ui.ts diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 892a99a77c..2201c75b4c 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -1,16 +1,25 @@ import { generateSpecs } from "hono-openapi" import { Hono } from "hono" +import type { MiddlewareHandler } from "hono" import { adapter } from "#hono" -import { MDNS } from "./mdns" import { lazy } from "@/util/lazy" -import { AuthMiddleware, CompressionMiddleware, CorsMiddleware, ErrorMiddleware, LoggerMiddleware } from "./middleware" -import { FenceMiddleware } from "./fence" -import { InstanceRoutes } from "./instance" -import { initProjectors } from "./projectors" import { Log } from "@/util" import { Flag } from "@/flag/flag" -import { ControlPlaneRoutes } from "./control" -import { UIRoutes } from "./ui" +import { Instance } from "@/project/instance" +import { InstanceBootstrap } from "@/project/bootstrap" +import { AppRuntime } from "@/effect/app-runtime" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { WorkspaceID } from "@/control-plane/schema" +import { WorkspaceContext } from "@/control-plane/workspace-context" +import { MDNS } from "./mdns" +import { AuthMiddleware, CompressionMiddleware, CorsMiddleware, ErrorMiddleware, LoggerMiddleware } from "./middleware" +import { FenceMiddleware } from "./fence" +import { initProjectors } from "./projectors" +import { InstanceRoutes } from "./routes/instance" +import { ControlPlaneRoutes } from "./routes/control" +import { UIRoutes } from "./routes/ui" +import { GlobalRoutes } from "./routes/global" +import { WorkspaceRouterMiddleware } from "./workspace" // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85 globalThis.AI_SDK_LOG_WARNINGS = false @@ -30,18 +39,48 @@ export const Default = lazy(() => create({})) function create(opts: { cors?: string[] }) { const app = new Hono() + .onError(ErrorMiddleware) + .use(AuthMiddleware) + .use(LoggerMiddleware) + .use(CompressionMiddleware) + .use(CorsMiddleware(opts)) + .route("/global", GlobalRoutes()) + const runtime = adapter.create(app) + function InstanceMiddleware(workspaceID?: WorkspaceID): MiddlewareHandler { + return async (c, next) => { + const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() + const directory = AppFileSystem.resolve( + (() => { + try { + return decodeURIComponent(raw) + } catch { + return raw + } + })(), + ) + + return WorkspaceContext.provide({ + workspaceID, + async fn() { + return Instance.provide({ + directory, + init: () => AppRuntime.runPromise(InstanceBootstrap), + async fn() { + return next() + }, + }) + }, + }) + } + } + if (Flag.OPENCODE_WORKSPACE_ID) { return { app: app - .onError(ErrorMiddleware) - .use(AuthMiddleware) - .use(LoggerMiddleware) - .use(CompressionMiddleware) - .use(CorsMiddleware(opts)) + .use(InstanceMiddleware(Flag.OPENCODE_WORKSPACE_ID ? WorkspaceID.make(Flag.OPENCODE_WORKSPACE_ID) : undefined)) .use(FenceMiddleware) - .route("/", ControlPlaneRoutes()) .route("/", InstanceRoutes(runtime.upgradeWebSocket)), runtime, } @@ -49,12 +88,9 @@ function create(opts: { cors?: string[] }) { return { app: app - .onError(ErrorMiddleware) - .use(AuthMiddleware) - .use(LoggerMiddleware) - .use(CompressionMiddleware) - .use(CorsMiddleware(opts)) + .use(InstanceMiddleware()) .route("/", ControlPlaneRoutes()) + .use(WorkspaceRouterMiddleware(runtime.upgradeWebSocket)) .route("/", InstanceRoutes(runtime.upgradeWebSocket)) .route("/", UIRoutes()), runtime, diff --git a/packages/opencode/src/server/instance/middleware.ts b/packages/opencode/src/server/workspace.ts similarity index 79% rename from packages/opencode/src/server/instance/middleware.ts rename to packages/opencode/src/server/workspace.ts index 7b66072c23..c141d10956 100644 --- a/packages/opencode/src/server/instance/middleware.ts +++ b/packages/opencode/src/server/workspace.ts @@ -2,17 +2,16 @@ import type { MiddlewareHandler } from "hono" import type { UpgradeWebSocket } from "hono/ws" import { getAdaptor } from "@/control-plane/adaptors" import { WorkspaceID } from "@/control-plane/schema" +import { WorkspaceContext } from "@/control-plane/workspace-context" import { Workspace } from "@/control-plane/workspace" -import { ServerProxy } from "../proxy" -import { Instance } from "@/project/instance" -import { InstanceBootstrap } from "@/project/bootstrap" import { Flag } from "@/flag/flag" +import { InstanceBootstrap } from "@/project/bootstrap" +import { Instance } from "@/project/instance" import { Session } from "@/session" import { SessionID } from "@/session/schema" -import { WorkspaceContext } from "@/control-plane/workspace-context" import { AppRuntime } from "@/effect/app-runtime" import { Log } from "@/util" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { ServerProxy } from "./proxy" type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" } @@ -51,45 +50,13 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware const log = Log.create({ service: "workspace-router" }) return async (c, next) => { - const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() - const directory = AppFileSystem.resolve( - (() => { - try { - return decodeURIComponent(raw) - } catch { - return raw - } - })(), - ) - const url = new URL(c.req.url) const sessionWorkspaceID = await getSessionWorkspace(url) const workspaceID = sessionWorkspaceID || url.searchParams.get("workspace") if (!workspaceID || url.pathname.startsWith("/console") || Flag.OPENCODE_WORKSPACE_ID) { - if (Flag.OPENCODE_WORKSPACE_ID) { - return WorkspaceContext.provide({ - workspaceID: WorkspaceID.make(Flag.OPENCODE_WORKSPACE_ID), - async fn() { - return Instance.provide({ - directory, - init: () => AppRuntime.runPromise(InstanceBootstrap), - async fn() { - return next() - }, - }) - }, - }) - } - - return Instance.provide({ - directory, - init: () => AppRuntime.runPromise(InstanceBootstrap), - async fn() { - return next() - }, - }) + return next() } const workspace = await Workspace.get(WorkspaceID.make(workspaceID)) diff --git a/packages/opencode/test/server/session-messages.test.ts b/packages/opencode/test/server/session-messages.test.ts index 50b7658969..23e8b50145 100644 --- a/packages/opencode/test/server/session-messages.test.ts +++ b/packages/opencode/test/server/session-messages.test.ts @@ -165,16 +165,3 @@ describe("session messages endpoint", () => { ) }) }) - -describe("session.prompt_async error handling", () => { - test("prompt_async route has error handler for detached prompt call", async () => { - const src = await Bun.file(new URL("../../src/server/instance/session.ts", import.meta.url)).text() - const start = src.indexOf('"/:sessionID/prompt_async"') - const end = src.indexOf('"/:sessionID/command"', start) - expect(start).toBeGreaterThan(-1) - expect(end).toBeGreaterThan(start) - const route = src.slice(start, end) - expect(route).toContain(".catch(") - expect(route).toContain("Bus.publish(Session.Event.Error") - }) -}) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index d7bf43f506..f484147a40 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -510,6 +510,430 @@ export class App extends HeyApiClient { } } +export class Adaptor extends HeyApiClient { + /** + * List workspace adaptors + * + * List all available workspace adaptors for the current project. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/workspace/adaptor", + ...options, + ...params, + }) + } +} + +export class Workspace extends HeyApiClient { + /** + * List workspaces + * + * List all workspaces. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/workspace", + ...options, + ...params, + }) + } + + /** + * Create workspace + * + * Create a workspace for the current project. + */ + public create( + parameters?: { + directory?: string + workspace?: string + id?: string + type?: string + branch?: string | null + extra?: unknown | null + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "id" }, + { in: "body", key: "type" }, + { in: "body", key: "branch" }, + { in: "body", key: "extra" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ExperimentalWorkspaceCreateResponses, + ExperimentalWorkspaceCreateErrors, + ThrowOnError + >({ + url: "/experimental/workspace", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Workspace status + * + * Get connection status for workspaces in the current project. + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/workspace/status", + ...options, + ...params, + }) + } + + /** + * Remove workspace + * + * Remove an existing workspace. + */ + public remove( + parameters: { + id: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "id" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete< + ExperimentalWorkspaceRemoveResponses, + ExperimentalWorkspaceRemoveErrors, + ThrowOnError + >({ + url: "/experimental/workspace/{id}", + ...options, + ...params, + }) + } + + /** + * Restore session into workspace + * + * Replay a session's sync events into the target workspace in batches. + */ + public sessionRestore( + parameters: { + id: string + directory?: string + workspace?: string + sessionID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "id" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "sessionID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ExperimentalWorkspaceSessionRestoreResponses, + ExperimentalWorkspaceSessionRestoreErrors, + ThrowOnError + >({ + url: "/experimental/workspace/{id}/session-restore", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + private _adaptor?: Adaptor + get adaptor(): Adaptor { + return (this._adaptor ??= new Adaptor({ client: this.client })) + } +} + +export class Console extends HeyApiClient { + /** + * Get active Console provider metadata + * + * Get the active Console org name and the set of provider IDs managed by that Console org. + */ + public get( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/console", + ...options, + ...params, + }) + } + + /** + * List switchable Console orgs + * + * Get the available Console orgs across logged-in accounts, including the current active org. + */ + public listOrgs( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/console/orgs", + ...options, + ...params, + }) + } + + /** + * Switch active Console org + * + * Persist a new active Console account/org selection for the current local OpenCode state. + */ + public switchOrg( + parameters?: { + directory?: string + workspace?: string + accountID?: string + orgID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "accountID" }, + { in: "body", key: "orgID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/experimental/console/switch", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Session extends HeyApiClient { + /** + * List sessions + * + * Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default. + */ + public list( + parameters?: { + directory?: string + workspace?: string + roots?: boolean + start?: number + cursor?: number + search?: string + limit?: number + archived?: boolean + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "roots" }, + { in: "query", key: "start" }, + { in: "query", key: "cursor" }, + { in: "query", key: "search" }, + { in: "query", key: "limit" }, + { in: "query", key: "archived" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/session", + ...options, + ...params, + }) + } +} + +export class Resource extends HeyApiClient { + /** + * Get MCP resources + * + * Get all available MCP resources from connected servers. Optionally filter by name. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/resource", + ...options, + ...params, + }) + } +} + +export class Experimental extends HeyApiClient { + private _workspace?: Workspace + get workspace(): Workspace { + return (this._workspace ??= new Workspace({ client: this.client })) + } + + private _console?: Console + get console(): Console { + return (this._console ??= new Console({ client: this.client })) + } + + private _session?: Session + get session(): Session { + return (this._session ??= new Session({ client: this.client })) + } + + private _resource?: Resource + get resource(): Resource { + return (this._resource ??= new Resource({ client: this.client })) + } +} + export class Project extends HeyApiClient { /** * List all projects @@ -972,430 +1396,6 @@ export class Config2 extends HeyApiClient { } } -export class Console extends HeyApiClient { - /** - * Get active Console provider metadata - * - * Get the active Console org name and the set of provider IDs managed by that Console org. - */ - public get( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/experimental/console", - ...options, - ...params, - }) - } - - /** - * List switchable Console orgs - * - * Get the available Console orgs across logged-in accounts, including the current active org. - */ - public listOrgs( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/experimental/console/orgs", - ...options, - ...params, - }) - } - - /** - * Switch active Console org - * - * Persist a new active Console account/org selection for the current local OpenCode state. - */ - public switchOrg( - parameters?: { - directory?: string - workspace?: string - accountID?: string - orgID?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "accountID" }, - { in: "body", key: "orgID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/experimental/console/switch", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Adaptor extends HeyApiClient { - /** - * List workspace adaptors - * - * List all available workspace adaptors for the current project. - */ - public list( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/experimental/workspace/adaptor", - ...options, - ...params, - }) - } -} - -export class Workspace extends HeyApiClient { - /** - * List workspaces - * - * List all workspaces. - */ - public list( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/experimental/workspace", - ...options, - ...params, - }) - } - - /** - * Create workspace - * - * Create a workspace for the current project. - */ - public create( - parameters?: { - directory?: string - workspace?: string - id?: string - type?: string - branch?: string | null - extra?: unknown | null - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "id" }, - { in: "body", key: "type" }, - { in: "body", key: "branch" }, - { in: "body", key: "extra" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - ExperimentalWorkspaceCreateResponses, - ExperimentalWorkspaceCreateErrors, - ThrowOnError - >({ - url: "/experimental/workspace", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Workspace status - * - * Get connection status for workspaces in the current project. - */ - public status( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/experimental/workspace/status", - ...options, - ...params, - }) - } - - /** - * Remove workspace - * - * Remove an existing workspace. - */ - public remove( - parameters: { - id: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "id" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete< - ExperimentalWorkspaceRemoveResponses, - ExperimentalWorkspaceRemoveErrors, - ThrowOnError - >({ - url: "/experimental/workspace/{id}", - ...options, - ...params, - }) - } - - /** - * Restore session into workspace - * - * Replay a session's sync events into the target workspace in batches. - */ - public sessionRestore( - parameters: { - id: string - directory?: string - workspace?: string - sessionID?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "id" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "sessionID" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - ExperimentalWorkspaceSessionRestoreResponses, - ExperimentalWorkspaceSessionRestoreErrors, - ThrowOnError - >({ - url: "/experimental/workspace/{id}/session-restore", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - private _adaptor?: Adaptor - get adaptor(): Adaptor { - return (this._adaptor ??= new Adaptor({ client: this.client })) - } -} - -export class Session extends HeyApiClient { - /** - * List sessions - * - * Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default. - */ - public list( - parameters?: { - directory?: string - workspace?: string - roots?: boolean - start?: number - cursor?: number - search?: string - limit?: number - archived?: boolean - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "query", key: "roots" }, - { in: "query", key: "start" }, - { in: "query", key: "cursor" }, - { in: "query", key: "search" }, - { in: "query", key: "limit" }, - { in: "query", key: "archived" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/experimental/session", - ...options, - ...params, - }) - } -} - -export class Resource extends HeyApiClient { - /** - * Get MCP resources - * - * Get all available MCP resources from connected servers. Optionally filter by name. - */ - public list( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/experimental/resource", - ...options, - ...params, - }) - } -} - -export class Experimental extends HeyApiClient { - private _console?: Console - get console(): Console { - return (this._console ??= new Console({ client: this.client })) - } - - private _workspace?: Workspace - get workspace(): Workspace { - return (this._workspace ??= new Workspace({ client: this.client })) - } - - private _session?: Session - get session(): Session { - return (this._session ??= new Session({ client: this.client })) - } - - private _resource?: Resource - get resource(): Resource { - return (this._resource ??= new Resource({ client: this.client })) - } -} - export class Tool extends HeyApiClient { /** * List tool IDs @@ -4314,6 +4314,11 @@ export class OpencodeClient extends HeyApiClient { return (this._app ??= new App({ client: this.client })) } + private _experimental?: Experimental + get experimental(): Experimental { + return (this._experimental ??= new Experimental({ client: this.client })) + } + private _project?: Project get project(): Project { return (this._project ??= new Project({ client: this.client })) @@ -4329,11 +4334,6 @@ export class OpencodeClient extends HeyApiClient { return (this._config ??= new Config2({ client: this.client })) } - private _experimental?: Experimental - get experimental(): Experimental { - return (this._experimental ??= new Experimental({ client: this.client })) - } - private _tool?: Tool get tool(): Tool { return (this._tool ??= new Tool({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 25c3cfa669..839dae8b22 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1706,6 +1706,16 @@ export type WellKnownAuth = { export type Auth = OAuth | ApiAuth | WellKnownAuth +export type Workspace = { + id: string + type: string + name: string + branch: string | null + directory: string | null + extra: unknown | null + projectID: string +} + export type NotFoundError = { name: "NotFoundError" data: { @@ -1808,16 +1818,6 @@ export type ToolListItem = { export type ToolList = Array -export type Workspace = { - id: string - type: string - name: string - branch: string | null - directory: string | null - extra: unknown | null - projectID: string -} - export type Worktree = { name: string branch: string @@ -2394,6 +2394,177 @@ export type AppLogResponses = { export type AppLogResponse = AppLogResponses[keyof AppLogResponses] +export type ExperimentalWorkspaceAdaptorListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace/adaptor" +} + +export type ExperimentalWorkspaceAdaptorListResponses = { + /** + * Workspace adaptors + */ + 200: Array<{ + type: string + name: string + description: string + }> +} + +export type ExperimentalWorkspaceAdaptorListResponse = + ExperimentalWorkspaceAdaptorListResponses[keyof ExperimentalWorkspaceAdaptorListResponses] + +export type ExperimentalWorkspaceListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace" +} + +export type ExperimentalWorkspaceListResponses = { + /** + * Workspaces + */ + 200: Array +} + +export type ExperimentalWorkspaceListResponse = + ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses] + +export type ExperimentalWorkspaceCreateData = { + body?: { + id?: string + type: string + branch: string | null + extra: unknown | null + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace" +} + +export type ExperimentalWorkspaceCreateErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalWorkspaceCreateError = + ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors] + +export type ExperimentalWorkspaceCreateResponses = { + /** + * Workspace created + */ + 200: Workspace +} + +export type ExperimentalWorkspaceCreateResponse = + ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses] + +export type ExperimentalWorkspaceStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace/status" +} + +export type ExperimentalWorkspaceStatusResponses = { + /** + * Workspace status + */ + 200: Array<{ + workspaceID: string + status: "connected" | "connecting" | "disconnected" | "error" + error?: string + }> +} + +export type ExperimentalWorkspaceStatusResponse = + ExperimentalWorkspaceStatusResponses[keyof ExperimentalWorkspaceStatusResponses] + +export type ExperimentalWorkspaceRemoveData = { + body?: never + path: { + id: string + } + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace/{id}" +} + +export type ExperimentalWorkspaceRemoveErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalWorkspaceRemoveError = + ExperimentalWorkspaceRemoveErrors[keyof ExperimentalWorkspaceRemoveErrors] + +export type ExperimentalWorkspaceRemoveResponses = { + /** + * Workspace removed + */ + 200: Workspace +} + +export type ExperimentalWorkspaceRemoveResponse = + ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses] + +export type ExperimentalWorkspaceSessionRestoreData = { + body?: { + sessionID: string + } + path: { + id: string + } + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace/{id}/session-restore" +} + +export type ExperimentalWorkspaceSessionRestoreErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalWorkspaceSessionRestoreError = + ExperimentalWorkspaceSessionRestoreErrors[keyof ExperimentalWorkspaceSessionRestoreErrors] + +export type ExperimentalWorkspaceSessionRestoreResponses = { + /** + * Session replay started + */ + 200: { + total: number + } +} + +export type ExperimentalWorkspaceSessionRestoreResponse = + ExperimentalWorkspaceSessionRestoreResponses[keyof ExperimentalWorkspaceSessionRestoreResponses] + export type ProjectListData = { body?: never path?: never @@ -2883,177 +3054,6 @@ export type ToolListResponses = { export type ToolListResponse = ToolListResponses[keyof ToolListResponses] -export type ExperimentalWorkspaceAdaptorListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace/adaptor" -} - -export type ExperimentalWorkspaceAdaptorListResponses = { - /** - * Workspace adaptors - */ - 200: Array<{ - type: string - name: string - description: string - }> -} - -export type ExperimentalWorkspaceAdaptorListResponse = - ExperimentalWorkspaceAdaptorListResponses[keyof ExperimentalWorkspaceAdaptorListResponses] - -export type ExperimentalWorkspaceListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace" -} - -export type ExperimentalWorkspaceListResponses = { - /** - * Workspaces - */ - 200: Array -} - -export type ExperimentalWorkspaceListResponse = - ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses] - -export type ExperimentalWorkspaceCreateData = { - body?: { - id?: string - type: string - branch: string | null - extra: unknown | null - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace" -} - -export type ExperimentalWorkspaceCreateErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalWorkspaceCreateError = - ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors] - -export type ExperimentalWorkspaceCreateResponses = { - /** - * Workspace created - */ - 200: Workspace -} - -export type ExperimentalWorkspaceCreateResponse = - ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses] - -export type ExperimentalWorkspaceStatusData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace/status" -} - -export type ExperimentalWorkspaceStatusResponses = { - /** - * Workspace status - */ - 200: Array<{ - workspaceID: string - status: "connected" | "connecting" | "disconnected" | "error" - error?: string - }> -} - -export type ExperimentalWorkspaceStatusResponse = - ExperimentalWorkspaceStatusResponses[keyof ExperimentalWorkspaceStatusResponses] - -export type ExperimentalWorkspaceRemoveData = { - body?: never - path: { - id: string - } - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace/{id}" -} - -export type ExperimentalWorkspaceRemoveErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalWorkspaceRemoveError = - ExperimentalWorkspaceRemoveErrors[keyof ExperimentalWorkspaceRemoveErrors] - -export type ExperimentalWorkspaceRemoveResponses = { - /** - * Workspace removed - */ - 200: Workspace -} - -export type ExperimentalWorkspaceRemoveResponse = - ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses] - -export type ExperimentalWorkspaceSessionRestoreData = { - body?: { - sessionID: string - } - path: { - id: string - } - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace/{id}/session-restore" -} - -export type ExperimentalWorkspaceSessionRestoreErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalWorkspaceSessionRestoreError = - ExperimentalWorkspaceSessionRestoreErrors[keyof ExperimentalWorkspaceSessionRestoreErrors] - -export type ExperimentalWorkspaceSessionRestoreResponses = { - /** - * Session replay started - */ - 200: { - total: number - } -} - -export type ExperimentalWorkspaceSessionRestoreResponse = - ExperimentalWorkspaceSessionRestoreResponses[keyof ExperimentalWorkspaceSessionRestoreResponses] - export type WorktreeRemoveData = { body?: WorktreeRemoveInput path?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 9193b11ad9..cf14026eae 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -415,6 +415,394 @@ ] } }, + "/experimental/workspace/adaptor": { + "get": { + "operationId": "experimental.workspace.adaptor.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + } + ], + "summary": "List workspace adaptors", + "description": "List all available workspace adaptors for the current project.", + "responses": { + "200": { + "description": "Workspace adaptors", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": ["type", "name", "description"] + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.adaptor.list({\n ...\n})" + } + ] + } + }, + "/experimental/workspace": { + "post": { + "operationId": "experimental.workspace.create", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + } + ], + "summary": "Create workspace", + "description": "Create a workspace for the current project.", + "responses": { + "200": { + "description": "Workspace created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Workspace" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^wrk.*" + }, + "type": { + "type": "string" + }, + "branch": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "extra": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + } + }, + "required": ["type", "branch", "extra"] + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.create({\n ...\n})" + } + ] + }, + "get": { + "operationId": "experimental.workspace.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + } + ], + "summary": "List workspaces", + "description": "List all workspaces.", + "responses": { + "200": { + "description": "Workspaces", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Workspace" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.list({\n ...\n})" + } + ] + } + }, + "/experimental/workspace/status": { + "get": { + "operationId": "experimental.workspace.status", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + } + ], + "summary": "Workspace status", + "description": "Get connection status for workspaces in the current project.", + "responses": { + "200": { + "description": "Workspace status", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "workspaceID": { + "type": "string", + "pattern": "^wrk.*" + }, + "status": { + "type": "string", + "enum": ["connected", "connecting", "disconnected", "error"] + }, + "error": { + "type": "string" + } + }, + "required": ["workspaceID", "status"] + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.status({\n ...\n})" + } + ] + } + }, + "/experimental/workspace/{id}": { + "delete": { + "operationId": "experimental.workspace.remove", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "id", + "schema": { + "type": "string", + "pattern": "^wrk.*" + }, + "required": true + } + ], + "summary": "Remove workspace", + "description": "Remove an existing workspace.", + "responses": { + "200": { + "description": "Workspace removed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Workspace" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.remove({\n ...\n})" + } + ] + } + }, + "/experimental/workspace/{id}/session-restore": { + "post": { + "operationId": "experimental.workspace.sessionRestore", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "id", + "schema": { + "type": "string", + "pattern": "^wrk.*" + }, + "required": true + } + ], + "summary": "Restore session into workspace", + "description": "Replay a session's sync events into the target workspace in batches.", + "responses": { + "200": { + "description": "Session replay started", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["total"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses.*" + } + }, + "required": ["sessionID"] + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.sessionRestore({\n ...\n})" + } + ] + } + }, "/project": { "get": { "operationId": "project.list", @@ -1501,394 +1889,6 @@ ] } }, - "/experimental/workspace/adaptor": { - "get": { - "operationId": "experimental.workspace.adaptor.list", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "workspace", - "schema": { - "type": "string" - } - } - ], - "summary": "List workspace adaptors", - "description": "List all available workspace adaptors for the current project.", - "responses": { - "200": { - "description": "Workspace adaptors", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["type", "name", "description"] - } - } - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.adaptor.list({\n ...\n})" - } - ] - } - }, - "/experimental/workspace": { - "post": { - "operationId": "experimental.workspace.create", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "workspace", - "schema": { - "type": "string" - } - } - ], - "summary": "Create workspace", - "description": "Create a workspace for the current project.", - "responses": { - "200": { - "description": "Workspace created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Workspace" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^wrk.*" - }, - "type": { - "type": "string" - }, - "branch": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "extra": { - "anyOf": [ - {}, - { - "type": "null" - } - ] - } - }, - "required": ["type", "branch", "extra"] - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.create({\n ...\n})" - } - ] - }, - "get": { - "operationId": "experimental.workspace.list", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "workspace", - "schema": { - "type": "string" - } - } - ], - "summary": "List workspaces", - "description": "List all workspaces.", - "responses": { - "200": { - "description": "Workspaces", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Workspace" - } - } - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.list({\n ...\n})" - } - ] - } - }, - "/experimental/workspace/status": { - "get": { - "operationId": "experimental.workspace.status", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "workspace", - "schema": { - "type": "string" - } - } - ], - "summary": "Workspace status", - "description": "Get connection status for workspaces in the current project.", - "responses": { - "200": { - "description": "Workspace status", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "workspaceID": { - "type": "string", - "pattern": "^wrk.*" - }, - "status": { - "type": "string", - "enum": ["connected", "connecting", "disconnected", "error"] - }, - "error": { - "type": "string" - } - }, - "required": ["workspaceID", "status"] - } - } - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.status({\n ...\n})" - } - ] - } - }, - "/experimental/workspace/{id}": { - "delete": { - "operationId": "experimental.workspace.remove", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "workspace", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "id", - "schema": { - "type": "string", - "pattern": "^wrk.*" - }, - "required": true - } - ], - "summary": "Remove workspace", - "description": "Remove an existing workspace.", - "responses": { - "200": { - "description": "Workspace removed", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Workspace" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.remove({\n ...\n})" - } - ] - } - }, - "/experimental/workspace/{id}/session-restore": { - "post": { - "operationId": "experimental.workspace.sessionRestore", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "workspace", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "id", - "schema": { - "type": "string", - "pattern": "^wrk.*" - }, - "required": true - } - ], - "summary": "Restore session into workspace", - "description": "Replay a session's sync events into the target workspace in batches.", - "responses": { - "200": { - "description": "Session replay started", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "total": { - "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 - } - }, - "required": ["total"] - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses.*" - } - }, - "required": ["sessionID"] - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.sessionRestore({\n ...\n})" - } - ] - } - }, "/experimental/worktree": { "post": { "operationId": "worktree.create", @@ -12003,6 +12003,53 @@ } ] }, + "Workspace": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^wrk.*" + }, + "type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "branch": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "extra": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + }, + "projectID": { + "type": "string" + } + }, + "required": ["id", "type", "name", "branch", "directory", "extra", "projectID"] + }, "NotFoundError": { "type": "object", "properties": { @@ -12309,53 +12356,6 @@ "$ref": "#/components/schemas/ToolListItem" } }, - "Workspace": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^wrk.*" - }, - "type": { - "type": "string" - }, - "name": { - "type": "string" - }, - "branch": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "directory": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "extra": { - "anyOf": [ - {}, - { - "type": "null" - } - ] - }, - "projectID": { - "type": "string" - } - }, - "required": ["id", "type", "name", "branch", "directory", "extra", "projectID"] - }, "Worktree": { "type": "object", "properties": { From 65b2a10e97d34e6b8a832a69b5a75f90f21e076c Mon Sep 17 00:00:00 2001 From: Dax Date: Fri, 17 Apr 2026 02:12:41 -0400 Subject: [PATCH 075/335] fade in prompt metadata transitions (#23037) --- packages/opencode/src/cli/cmd/tui/app.tsx | 18 ++++---- .../cli/cmd/tui/component/prompt/index.tsx | 44 ++++++++++++++++--- .../src/cli/cmd/tui/context/route.tsx | 13 +++--- .../opencode/src/cli/cmd/tui/context/sync.tsx | 1 + .../opencode/src/cli/cmd/tui/util/signal.ts | 33 +++++++++++++- 5 files changed, 87 insertions(+), 22 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 7e883ec0e3..74eca9a0f2 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -148,7 +148,16 @@ export function tui(input: { - + Promise }) { }) local.model.set({ providerID, modelID }, { recent: true }) } - // Handle --session without --fork immediately (fork is handled in createEffect below) - if (args.sessionID && !args.fork) { - route.navigate({ - type: "session", - sessionID: args.sessionID, - }) - } }) }) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 82cdefebcb..08540e62e4 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1,5 +1,15 @@ -import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, decodePasteBytes } from "@opentui/core" -import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js" +import { BoxRenderable, RGBA, TextareaRenderable, MouseEvent, PasteEvent, decodePasteBytes } from "@opentui/core" +import { + createEffect, + createMemo, + onMount, + createSignal, + onCleanup, + on, + Show, + Switch, + Match, +} from "solid-js" import "opentui-spinner/solid" import path from "path" import { fileURLToPath } from "url" @@ -35,6 +45,7 @@ import { DialogProvider as DialogProviderConnect } from "../dialog-provider" import { DialogAlert } from "../../ui/dialog-alert" import { useToast } from "../../ui/toast" import { useKV } from "../../context/kv" +import { createFadeIn } from "../../util/signal" import { useTextareaKeybindings } from "../textarea-keybindings" import { DialogSkill } from "../dialog-skill" import { useArgs } from "@tui/context/args" @@ -75,6 +86,10 @@ function randomIndex(count: number) { return Math.floor(Math.random() * count) } +function fadeColor(color: RGBA, alpha: number) { + return RGBA.fromValues(color.r, color.g, color.b, color.a * alpha) +} + let stashed: { prompt: PromptInfo; cursor: number } | undefined export function Prompt(props: PromptProps) { @@ -97,6 +112,7 @@ export function Prompt(props: PromptProps) { const renderer = useRenderer() const { theme, syntax } = useTheme() const kv = useKV() + const animationsEnabled = createMemo(() => kv.get("animations_enabled", true)) const list = createMemo(() => props.placeholders?.normal ?? []) const shell = createMemo(() => props.placeholders?.shell ?? []) const [auto, setAuto] = createSignal() @@ -858,6 +874,13 @@ export function Prompt(props: PromptProps) { return !!current }) + const agentMetaAlpha = createFadeIn(() => !!local.agent.current(), animationsEnabled) + const modelMetaAlpha = createFadeIn(() => !!local.agent.current() && store.mode === "normal", animationsEnabled) + const variantMetaAlpha = createFadeIn( + () => !!local.agent.current() && store.mode === "normal" && showVariant(), + animationsEnabled, + ) + const placeholderText = createMemo(() => { if (props.showPlaceholder === false) return undefined if (store.mode === "shell") { @@ -1133,17 +1156,24 @@ export function Prompt(props: PromptProps) { }> {(agent) => ( <> - {store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)} + + {store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)}{" "} + - + {local.model.parsed().model} - {currentProviderLabel()} + {currentProviderLabel()} - · + · - {local.model.variant.current()} + + {local.model.variant.current()} + diff --git a/packages/opencode/src/cli/cmd/tui/context/route.tsx b/packages/opencode/src/cli/cmd/tui/context/route.tsx index 6db8247592..35be17801b 100644 --- a/packages/opencode/src/cli/cmd/tui/context/route.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/route.tsx @@ -23,13 +23,14 @@ export type Route = HomeRoute | SessionRoute | PluginRoute export const { use: useRoute, provider: RouteProvider } = createSimpleContext({ name: "Route", - init: () => { + init: (props: { initialRoute?: Route }) => { const [store, setStore] = createStore( - process.env["OPENCODE_ROUTE"] - ? JSON.parse(process.env["OPENCODE_ROUTE"]) - : { - type: "home", - }, + props.initialRoute ?? + (process.env["OPENCODE_ROUTE"] + ? JSON.parse(process.env["OPENCODE_ROUTE"]) + : { + type: "home", + }), ) return { diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 57326e3a1a..d2a7e5c4d0 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -467,6 +467,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ return store.status }, get ready() { + return true if (process.env.OPENCODE_FAST_BOOT) return true return store.status !== "loading" }, diff --git a/packages/opencode/src/cli/cmd/tui/util/signal.ts b/packages/opencode/src/cli/cmd/tui/util/signal.ts index 15b57886d6..1c7cc0008d 100644 --- a/packages/opencode/src/cli/cmd/tui/util/signal.ts +++ b/packages/opencode/src/cli/cmd/tui/util/signal.ts @@ -1,7 +1,38 @@ -import { createSignal, type Accessor } from "solid-js" +import { createEffect, createSignal, on, onCleanup, type Accessor } from "solid-js" import { debounce, type Scheduled } from "@solid-primitives/scheduled" export function createDebouncedSignal(value: T, ms: number): [Accessor, Scheduled<[value: T]>] { const [get, set] = createSignal(value) return [get, debounce((v: T) => set(() => v), ms)] } + +export function createFadeIn(show: Accessor, enabled: Accessor) { + const [alpha, setAlpha] = createSignal(show() ? 1 : 0) + + createEffect( + on([show, enabled], ([visible, animate], previous) => { + if (!visible) { + setAlpha(0) + return + } + + if (!animate || !previous) { + setAlpha(1) + return + } + + const start = performance.now() + setAlpha(0) + + const timer = setInterval(() => { + const progress = Math.min((performance.now() - start) / 160, 1) + setAlpha(progress * progress * (3 - 2 * progress)) + if (progress >= 1) clearInterval(timer) + }, 16) + + onCleanup(() => clearInterval(timer)) + }), + ) + + return alpha +} From 81f0885879f1e89b21774fc1fc6c603bd0ecd967 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 06:13:42 +0000 Subject: [PATCH 076/335] chore: generate --- .../src/cli/cmd/tui/component/prompt/index.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 08540e62e4..06e5a0884e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1,15 +1,5 @@ import { BoxRenderable, RGBA, TextareaRenderable, MouseEvent, PasteEvent, decodePasteBytes } from "@opentui/core" -import { - createEffect, - createMemo, - onMount, - createSignal, - onCleanup, - on, - Show, - Switch, - Match, -} from "solid-js" +import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js" import "opentui-spinner/solid" import path from "path" import { fileURLToPath } from "url" From d9950598d0da16fc0f8e6c289050a9d3da055af7 Mon Sep 17 00:00:00 2001 From: Dax Date: Fri, 17 Apr 2026 02:44:01 -0400 Subject: [PATCH 077/335] core: migrate config loading to Effect framework (#23032) --- packages/opencode/src/cli/cmd/tui/app.tsx | 2 + .../src/cli/cmd/tui/config/tui-migrate.ts | 6 +- .../opencode/src/cli/cmd/tui/config/tui.ts | 27 +- packages/opencode/src/config/config.ts | 457 +++++++++--------- packages/opencode/src/config/paths.ts | 46 +- 5 files changed, 275 insertions(+), 263 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 74eca9a0f2..f8ffd27dc8 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -135,7 +135,9 @@ export function tui(input: { await TuiPluginRuntime.dispose() } + console.log("starting renderer") const renderer = await createCliRenderer(rendererConfig(input.config)) + console.log("renderer started") await render(() => { return ( diff --git a/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts b/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts index 9323dd979a..a7f50ddf9d 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts @@ -132,8 +132,10 @@ async function backupAndStripLegacy(file: string, source: string) { } async function opencodeFiles(input: { directories: string[]; cwd: string }) { - const project = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("opencode", input.cwd) - const files = [...project, ...ConfigPaths.fileInDirectory(Global.Path.config, "opencode")] + const files = [ + ...ConfigPaths.fileInDirectory(Global.Path.config, "opencode"), + ...(await Filesystem.findUp(["opencode.json", "opencode.jsonc"], input.cwd, undefined, { rootFirst: true })), + ] for (const dir of unique(input.directories)) { files.push(...ConfigPaths.fileInDirectory(dir, "opencode")) } diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index 1a5e49badb..a5c9ae0430 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -89,15 +89,13 @@ async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) { acc.result.plugin_origins = plugins } -async function loadState(ctx: { directory: string }) { +const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory: string }) { // Every config dir we may read from: global config dir, any `.opencode` // folders between cwd and home, and OPENCODE_CONFIG_DIR. - const directories = await ConfigPaths.directories(ctx.directory) - // One-time migration: extract tui keys (theme/keybinds/tui) from existing - // opencode.json files into sibling tui.json files. - await migrateTuiConfig({ directories, cwd: ctx.directory }) + const directories = yield* ConfigPaths.directories(ctx.directory) + yield* Effect.promise(() => migrateTuiConfig({ directories, cwd: ctx.directory })) - const projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("tui", ctx.directory) + const projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : yield* ConfigPaths.files("tui", ctx.directory) const acc: Acc = { result: {}, @@ -105,18 +103,19 @@ async function loadState(ctx: { directory: string }) { // 1. Global tui config (lowest precedence). for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) { - await mergeFile(acc, file, ctx) + yield* Effect.promise(() => mergeFile(acc, file, ctx)).pipe(Effect.orDie) } // 2. Explicit OPENCODE_TUI_CONFIG override, if set. if (Flag.OPENCODE_TUI_CONFIG) { - await mergeFile(acc, Flag.OPENCODE_TUI_CONFIG, ctx) - log.debug("loaded custom tui config", { path: Flag.OPENCODE_TUI_CONFIG }) + const configFile = Flag.OPENCODE_TUI_CONFIG + yield* Effect.promise(() => mergeFile(acc, configFile, ctx)).pipe(Effect.orDie) + log.debug("loaded custom tui config", { path: configFile }) } // 3. Project tui files, applied root-first so the closest file wins. for (const file of projectFiles) { - await mergeFile(acc, file, ctx) + yield* Effect.promise(() => mergeFile(acc, file, ctx)).pipe(Effect.orDie) } // 4. `.opencode` directories (and OPENCODE_CONFIG_DIR) discovered while @@ -127,7 +126,7 @@ async function loadState(ctx: { directory: string }) { for (const dir of dirs) { if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue for (const file of ConfigPaths.fileInDirectory(dir, "tui")) { - await mergeFile(acc, file, ctx) + yield* Effect.promise(() => mergeFile(acc, file, ctx)).pipe(Effect.orDie) } } @@ -146,14 +145,14 @@ async function loadState(ctx: { directory: string }) { config: acc.result, dirs: acc.result.plugin?.length ? dirs : [], } -} +}) export const layer = Layer.effect( Service, Effect.gen(function* () { const directory = yield* CurrentWorkingDirectory const npm = yield* Npm.Service - const data = yield* Effect.promise(() => loadState({ directory })) + const data = yield* loadState({ directory }) const deps = yield* Effect.forEach( data.dirs, (dir) => @@ -176,7 +175,7 @@ export const layer = Layer.effect( }).pipe(Effect.withSpan("TuiConfig.layer")), ) -export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer)) +export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer), Layer.provide(AppFileSystem.defaultLayer)) const { runPromise } = makeRuntime(Service, defaultLayer) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 7598aa92f1..ebd4a41fcb 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -413,260 +413,261 @@ export const layer = Layer.effect( } }) - const loadInstanceState = Effect.fn("Config.loadInstanceState")(function* (ctx: InstanceContext) { - const auth = yield* authSvc.all().pipe(Effect.orDie) + const loadInstanceState = Effect.fn("Config.loadInstanceState")( + function* (ctx: InstanceContext) { + const auth = yield* authSvc.all().pipe(Effect.orDie) - let result: Info = {} - const consoleManagedProviders = new Set() - let activeOrgName: string | undefined + let result: Info = {} + const consoleManagedProviders = new Set() + let activeOrgName: string | undefined - const pluginScopeForSource = Effect.fnUntraced(function* (source: string) { - if (source.startsWith("http://") || source.startsWith("https://")) return "global" - if (source === "OPENCODE_CONFIG_CONTENT") return "local" - if (yield* InstanceRef.use((ctx) => Effect.succeed(Instance.containsPath(source, ctx)))) return "local" - return "global" - }) - - const mergePluginOrigins = Effect.fnUntraced(function* ( - source: string, - // mergePluginOrigins receives raw Specs from one config source, before provenance for this merge step - // is attached. - list: ConfigPlugin.Spec[] | undefined, - // Scope can be inferred from the source path, but some callers already know whether the config should - // behave as global or local and can pass that explicitly. - kind?: ConfigPlugin.Scope, - ) { - if (!list?.length) return - const hit = kind ?? (yield* pluginScopeForSource(source)) - // Merge newly seen plugin origins with previously collected ones, then dedupe by plugin identity while - // keeping the winning source/scope metadata for downstream installs, writes, and diagnostics. - const plugins = ConfigPlugin.deduplicatePluginOrigins([ - ...(result.plugin_origins ?? []), - ...list.map((spec) => ({ spec, source, scope: hit })), - ]) - result.plugin = plugins.map((item) => item.spec) - result.plugin_origins = plugins - }) - - const merge = (source: string, next: Info, kind?: ConfigPlugin.Scope) => { - result = mergeConfigConcatArrays(result, next) - return mergePluginOrigins(source, next.plugin, kind) - } - - for (const [key, value] of Object.entries(auth)) { - if (value.type === "wellknown") { - const url = key.replace(/\/+$/, "") - process.env[value.key] = value.token - log.debug("fetching remote config", { url: `${url}/.well-known/opencode` }) - const response = yield* Effect.promise(() => fetch(`${url}/.well-known/opencode`)) - if (!response.ok) { - throw new Error(`failed to fetch remote config from ${url}: ${response.status}`) - } - const wellknown = (yield* Effect.promise(() => response.json())) as { config?: Record } - const remoteConfig = wellknown.config ?? {} - if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json" - const source = `${url}/.well-known/opencode` - const next = yield* loadConfig(JSON.stringify(remoteConfig), { - dir: path.dirname(source), - source, - }) - yield* merge(source, next, "global") - log.debug("loaded remote config from well-known", { url }) - } - } - - const global = yield* getGlobal() - yield* merge(Global.Path.config, global, "global") - - if (Flag.OPENCODE_CONFIG) { - yield* merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG)) - log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG }) - } - - if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) { - for (const file of yield* Effect.promise(() => - ConfigPaths.projectFiles("opencode", ctx.directory, ctx.worktree), - )) { - yield* merge(file, yield* loadFile(file), "local") - } - } - - result.agent = result.agent || {} - result.mode = result.mode || {} - result.plugin = result.plugin || [] - - const directories = yield* Effect.promise(() => ConfigPaths.directories(ctx.directory, ctx.worktree)) - - if (Flag.OPENCODE_CONFIG_DIR) { - log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR }) - } - - const deps: Fiber.Fiber[] = [] - - for (const dir of directories) { - if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) { - for (const file of ["opencode.json", "opencode.jsonc"]) { - const source = path.join(dir, file) - log.debug(`loading config from ${source}`) - yield* merge(source, yield* loadFile(source)) - result.agent ??= {} - result.mode ??= {} - result.plugin ??= [] - } - } - - yield* ensureGitignore(dir).pipe(Effect.orDie) - - const dep = yield* npmSvc - .install(dir, { - add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)], - }) - .pipe( - Effect.exit, - Effect.tap((exit) => - Exit.isFailure(exit) - ? Effect.sync(() => { - log.warn("background dependency install failed", { dir, error: String(exit.cause) }) - }) - : Effect.void, - ), - Effect.asVoid, - Effect.forkDetach, - ) - deps.push(dep) - - result.command = mergeDeep(result.command ?? {}, yield* Effect.promise(() => ConfigCommand.load(dir))) - result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.load(dir))) - result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.loadMode(dir))) - // Auto-discovered plugins under `.opencode/plugin(s)` are already local files, so ConfigPlugin.load - // returns normalized Specs and we only need to attach origin metadata here. - const list = yield* Effect.promise(() => ConfigPlugin.load(dir)) - yield* mergePluginOrigins(dir, list) - } - - if (process.env.OPENCODE_CONFIG_CONTENT) { - const source = "OPENCODE_CONFIG_CONTENT" - const next = yield* loadConfig(process.env.OPENCODE_CONFIG_CONTENT, { - dir: ctx.directory, - source, + const pluginScopeForSource = Effect.fnUntraced(function* (source: string) { + if (source.startsWith("http://") || source.startsWith("https://")) return "global" + if (source === "OPENCODE_CONFIG_CONTENT") return "local" + if (yield* InstanceRef.use((ctx) => Effect.succeed(Instance.containsPath(source, ctx)))) return "local" + return "global" }) - yield* merge(source, next, "local") - log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT") - } - const activeAccount = Option.getOrUndefined( - yield* accountSvc.active().pipe(Effect.catch(() => Effect.succeed(Option.none()))), - ) - if (activeAccount?.active_org_id) { - const accountID = activeAccount.id - const orgID = activeAccount.active_org_id - const url = activeAccount.url - yield* Effect.gen(function* () { - const [configOpt, tokenOpt] = yield* Effect.all( - [accountSvc.config(accountID, orgID), accountSvc.token(accountID)], - { concurrency: 2 }, - ) - if (Option.isSome(tokenOpt)) { - process.env["OPENCODE_CONSOLE_TOKEN"] = tokenOpt.value - yield* env.set("OPENCODE_CONSOLE_TOKEN", tokenOpt.value) - } + const mergePluginOrigins = Effect.fnUntraced(function* ( + source: string, + // mergePluginOrigins receives raw Specs from one config source, before provenance for this merge step + // is attached. + list: ConfigPlugin.Spec[] | undefined, + // Scope can be inferred from the source path, but some callers already know whether the config should + // behave as global or local and can pass that explicitly. + kind?: ConfigPlugin.Scope, + ) { + if (!list?.length) return + const hit = kind ?? (yield* pluginScopeForSource(source)) + // Merge newly seen plugin origins with previously collected ones, then dedupe by plugin identity while + // keeping the winning source/scope metadata for downstream installs, writes, and diagnostics. + const plugins = ConfigPlugin.deduplicatePluginOrigins([ + ...(result.plugin_origins ?? []), + ...list.map((spec) => ({ spec, source, scope: hit })), + ]) + result.plugin = plugins.map((item) => item.spec) + result.plugin_origins = plugins + }) - if (Option.isSome(configOpt)) { - const source = `${url}/api/config` - const next = yield* loadConfig(JSON.stringify(configOpt.value), { + const merge = (source: string, next: Info, kind?: ConfigPlugin.Scope) => { + result = mergeConfigConcatArrays(result, next) + return mergePluginOrigins(source, next.plugin, kind) + } + + for (const [key, value] of Object.entries(auth)) { + if (value.type === "wellknown") { + const url = key.replace(/\/+$/, "") + process.env[value.key] = value.token + log.debug("fetching remote config", { url: `${url}/.well-known/opencode` }) + const response = yield* Effect.promise(() => fetch(`${url}/.well-known/opencode`)) + if (!response.ok) { + throw new Error(`failed to fetch remote config from ${url}: ${response.status}`) + } + const wellknown = (yield* Effect.promise(() => response.json())) as { config?: Record } + const remoteConfig = wellknown.config ?? {} + if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json" + const source = `${url}/.well-known/opencode` + const next = yield* loadConfig(JSON.stringify(remoteConfig), { dir: path.dirname(source), source, }) - for (const providerID of Object.keys(next.provider ?? {})) { - consoleManagedProviders.add(providerID) - } yield* merge(source, next, "global") + log.debug("loaded remote config from well-known", { url }) } - }).pipe( - Effect.withSpan("Config.loadActiveOrgConfig"), - Effect.catch((err) => { - log.debug("failed to fetch remote account config", { - error: err instanceof Error ? err.message : String(err), + } + + const global = yield* getGlobal() + yield* merge(Global.Path.config, global, "global") + + if (Flag.OPENCODE_CONFIG) { + yield* merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG)) + log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG }) + } + + if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) { + for (const file of yield* ConfigPaths.files("opencode", ctx.directory, ctx.worktree).pipe(Effect.orDie)) { + yield* merge(file, yield* loadFile(file), "local") + } + } + + result.agent = result.agent || {} + result.mode = result.mode || {} + result.plugin = result.plugin || [] + + const directories = yield* ConfigPaths.directories(ctx.directory, ctx.worktree) + + if (Flag.OPENCODE_CONFIG_DIR) { + log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR }) + } + + const deps: Fiber.Fiber[] = [] + + for (const dir of directories) { + if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) { + for (const file of ["opencode.json", "opencode.jsonc"]) { + const source = path.join(dir, file) + log.debug(`loading config from ${source}`) + yield* merge(source, yield* loadFile(source)) + result.agent ??= {} + result.mode ??= {} + result.plugin ??= [] + } + } + + yield* ensureGitignore(dir).pipe(Effect.orDie) + + const dep = yield* npmSvc + .install(dir, { + add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)], }) - return Effect.void - }), - ) - } + .pipe( + Effect.exit, + Effect.tap((exit) => + Exit.isFailure(exit) + ? Effect.sync(() => { + log.warn("background dependency install failed", { dir, error: String(exit.cause) }) + }) + : Effect.void, + ), + Effect.asVoid, + Effect.forkDetach, + ) + deps.push(dep) - const managedDir = ConfigManaged.managedConfigDir() - if (existsSync(managedDir)) { - for (const file of ["opencode.json", "opencode.jsonc"]) { - const source = path.join(managedDir, file) - yield* merge(source, yield* loadFile(source), "global") + result.command = mergeDeep(result.command ?? {}, yield* Effect.promise(() => ConfigCommand.load(dir))) + result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.load(dir))) + result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.loadMode(dir))) + // Auto-discovered plugins under `.opencode/plugin(s)` are already local files, so ConfigPlugin.load + // returns normalized Specs and we only need to attach origin metadata here. + const list = yield* Effect.promise(() => ConfigPlugin.load(dir)) + yield* mergePluginOrigins(dir, list) } - } - // macOS managed preferences (.mobileconfig deployed via MDM) override everything - const managed = yield* Effect.promise(() => ConfigManaged.readManagedPreferences()) - if (managed) { - result = mergeConfigConcatArrays( - result, - yield* loadConfig(managed.text, { - dir: path.dirname(managed.source), - source: managed.source, - }), + if (process.env.OPENCODE_CONFIG_CONTENT) { + const source = "OPENCODE_CONFIG_CONTENT" + const next = yield* loadConfig(process.env.OPENCODE_CONFIG_CONTENT, { + dir: ctx.directory, + source, + }) + yield* merge(source, next, "local") + log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT") + } + + const activeAccount = Option.getOrUndefined( + yield* accountSvc.active().pipe(Effect.catch(() => Effect.succeed(Option.none()))), ) - } + if (activeAccount?.active_org_id) { + const accountID = activeAccount.id + const orgID = activeAccount.active_org_id + const url = activeAccount.url + yield* Effect.gen(function* () { + const [configOpt, tokenOpt] = yield* Effect.all( + [accountSvc.config(accountID, orgID), accountSvc.token(accountID)], + { concurrency: 2 }, + ) + if (Option.isSome(tokenOpt)) { + process.env["OPENCODE_CONSOLE_TOKEN"] = tokenOpt.value + yield* env.set("OPENCODE_CONSOLE_TOKEN", tokenOpt.value) + } - for (const [name, mode] of Object.entries(result.mode ?? {})) { - result.agent = mergeDeep(result.agent ?? {}, { - [name]: { - ...mode, - mode: "primary" as const, - }, - }) - } + if (Option.isSome(configOpt)) { + const source = `${url}/api/config` + const next = yield* loadConfig(JSON.stringify(configOpt.value), { + dir: path.dirname(source), + source, + }) + for (const providerID of Object.keys(next.provider ?? {})) { + consoleManagedProviders.add(providerID) + } + yield* merge(source, next, "global") + } + }).pipe( + Effect.withSpan("Config.loadActiveOrgConfig"), + Effect.catch((err) => { + log.debug("failed to fetch remote account config", { + error: err instanceof Error ? err.message : String(err), + }) + return Effect.void + }), + ) + } - if (Flag.OPENCODE_PERMISSION) { - result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION)) - } - - if (result.tools) { - const perms: Record = {} - for (const [tool, enabled] of Object.entries(result.tools)) { - const action: ConfigPermission.Action = enabled ? "allow" : "deny" - if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") { - perms.edit = action - continue + const managedDir = ConfigManaged.managedConfigDir() + if (existsSync(managedDir)) { + for (const file of ["opencode.json", "opencode.jsonc"]) { + const source = path.join(managedDir, file) + yield* merge(source, yield* loadFile(source), "global") } - perms[tool] = action } - result.permission = mergeDeep(perms, result.permission ?? {}) - } - if (!result.username) result.username = os.userInfo().username + // macOS managed preferences (.mobileconfig deployed via MDM) override everything + const managed = yield* Effect.promise(() => ConfigManaged.readManagedPreferences()) + if (managed) { + result = mergeConfigConcatArrays( + result, + yield* loadConfig(managed.text, { + dir: path.dirname(managed.source), + source: managed.source, + }), + ) + } - if (result.autoshare === true && !result.share) { - result.share = "auto" - } + for (const [name, mode] of Object.entries(result.mode ?? {})) { + result.agent = mergeDeep(result.agent ?? {}, { + [name]: { + ...mode, + mode: "primary" as const, + }, + }) + } - if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) { - result.compaction = { ...result.compaction, auto: false } - } - if (Flag.OPENCODE_DISABLE_PRUNE) { - result.compaction = { ...result.compaction, prune: false } - } + if (Flag.OPENCODE_PERMISSION) { + result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION)) + } - return { - config: result, - directories, - deps, - consoleState: { - consoleManagedProviders: Array.from(consoleManagedProviders), - activeOrgName, - switchableOrgCount: 0, - }, - } - }) + if (result.tools) { + const perms: Record = {} + for (const [tool, enabled] of Object.entries(result.tools)) { + const action: ConfigPermission.Action = enabled ? "allow" : "deny" + if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") { + perms.edit = action + continue + } + perms[tool] = action + } + result.permission = mergeDeep(perms, result.permission ?? {}) + } + + if (!result.username) result.username = os.userInfo().username + + if (result.autoshare === true && !result.share) { + result.share = "auto" + } + + if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) { + result.compaction = { ...result.compaction, auto: false } + } + if (Flag.OPENCODE_DISABLE_PRUNE) { + result.compaction = { ...result.compaction, prune: false } + } + + return { + config: result, + directories, + deps, + consoleState: { + consoleManagedProviders: Array.from(consoleManagedProviders), + activeOrgName, + switchableOrgCount: 0, + }, + } + }, + Effect.provideService(AppFileSystem.Service, fs), + ) const state = yield* InstanceState.make( Effect.fn("Config.state")(function* (ctx) { - return yield* loadInstanceState(ctx) + return yield* loadInstanceState(ctx).pipe(Effect.orDie) }), ) diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts index dcf0c940f2..db4b914f76 100644 --- a/packages/opencode/src/config/paths.ts +++ b/packages/opencode/src/config/paths.ts @@ -6,33 +6,41 @@ import { Flag } from "@/flag/flag" import { Global } from "@/global" import { unique } from "remeda" import { JsonError } from "./error" +import * as Effect from "effect/Effect" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" -export async function projectFiles(name: string, directory: string, worktree?: string) { - return Filesystem.findUp([`${name}.json`, `${name}.jsonc`], directory, worktree, { rootFirst: true }) -} +export const files = Effect.fn("ConfigPaths.projectFiles")(function* ( + name: string, + directory: string, + worktree?: string, +) { + const afs = yield* AppFileSystem.Service + return (yield* afs.up({ + targets: [`${name}.jsonc`, `${name}.json`], + start: directory, + stop: worktree, + })).toReversed() +}) -export async function directories(directory: string, worktree?: string) { +export const directories = Effect.fn("ConfigPaths.directories")(function* (directory: string, worktree?: string) { + const afs = yield* AppFileSystem.Service return unique([ Global.Path.config, ...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG - ? await Array.fromAsync( - Filesystem.up({ - targets: [".opencode"], - start: directory, - stop: worktree, - }), - ) + ? yield* afs.up({ + targets: [".opencode"], + start: directory, + stop: worktree, + }) : []), - ...(await Array.fromAsync( - Filesystem.up({ - targets: [".opencode"], - start: Global.Path.home, - stop: Global.Path.home, - }), - )), + ...(yield* afs.up({ + targets: [".opencode"], + start: Global.Path.home, + stop: Global.Path.home, + })), ...(Flag.OPENCODE_CONFIG_DIR ? [Flag.OPENCODE_CONFIG_DIR] : []), ]) -} +}) export function fileInDirectory(dir: string, name: string) { return [path.join(dir, `${name}.json`), path.join(dir, `${name}.jsonc`)] From a7265307355e7efed9e276da6625c96458009db6 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:26:14 +0800 Subject: [PATCH 078/335] fix(app): workspace loading and persist ready state (#23046) --- packages/app/src/context/global-sync.tsx | 1 - .../src/context/global-sync/child-store.ts | 1 - packages/app/src/context/global-sync/types.ts | 1 - packages/app/src/pages/layout.tsx | 362 +++++++++--------- .../src/pages/layout/sidebar-workspace.tsx | 12 +- packages/app/src/utils/persist.ts | 2 +- 6 files changed, 186 insertions(+), 193 deletions(-) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 1359b07b4e..1a672639b5 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -264,7 +264,6 @@ function createGlobalSync() { children.pin(directory) const promise = Promise.resolve().then(async () => { const child = children.ensureChild(directory) - child[1]("bootstrapPromise", promise!) const cache = children.vcsCache.get(directory) if (!cache) return const sdk = sdkFor(directory) diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index 6788e8cc59..3fe67e4fbe 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -182,7 +182,6 @@ export function createChildStoreManager(input: { limit: 5, message: {}, part: {}, - bootstrapPromise: Promise.resolve(), }) children[directory] = child disposers.set(directory, dispose) diff --git a/packages/app/src/context/global-sync/types.ts b/packages/app/src/context/global-sync/types.ts index 28b3705d15..e3ec83c5ee 100644 --- a/packages/app/src/context/global-sync/types.ts +++ b/packages/app/src/context/global-sync/types.ts @@ -72,7 +72,6 @@ export type State = { part: { [messageID: string]: Part[] } - bootstrapPromise: Promise } export type VcsCache = { diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 12a2bf763a..6f6b3c5557 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -13,7 +13,7 @@ import { type Accessor, } from "solid-js" import { makeEventListener } from "@solid-primitives/event-listener" -import { useNavigate, useParams } from "@solidjs/router" +import { useLocation, useNavigate, useParams } from "@solidjs/router" import { useLayout, LocalProject } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" import { Persist, persisted } from "@/utils/persist" @@ -127,6 +127,7 @@ export default function Layout(props: ParentProps) { const theme = useTheme() const language = useLanguage() const initialDirectory = decode64(params.dir) + const location = useLocation() const route = createMemo(() => { const slug = params.dir if (!slug) return { slug, dir: "" } @@ -576,7 +577,7 @@ export default function Layout(props: ParentProps) { return projects.find((p) => p.worktree === root) }) - + const [autoselecting] = createResource(async () => { await ready.promise await layout.ready.promise @@ -2102,196 +2103,198 @@ export default function Layout(props: ParentProps) { } > - <> -
-
-
- { - const item = project() - if (!item) return - void renameProject(item, next) - }} - class="text-14-medium text-text-strong truncate" - displayClass="text-14-medium text-text-strong truncate" - stopPropagation - /> + {(project) => ( + <> +
+
+
+ { + const item = project() + if (!item) return + void renameProject(item, next) + }} + class="text-14-medium text-text-strong truncate" + displayClass="text-14-medium text-text-strong truncate" + stopPropagation + /> - - - {worktree().replace(homedir(), "~")} - - + + + {worktree().replace(homedir(), "~")} + + +
+ + + + + + { + const item = project() + if (!item) return + showEditProjectDialog(item) + }} + > + {language.t("common.edit")} + + { + const item = project() + if (!item) return + toggleProjectWorkspaces(item) + }} + > + + {workspacesEnabled() + ? language.t("sidebar.workspaces.disable") + : language.t("sidebar.workspaces.enable")} + + + + + {language.t("sidebar.project.clearNotifications")} + + + + { + const dir = worktree() + if (!dir) return + closeProject(dir) + }} + > + {language.t("common.close")} + + + +
- - - - - - { - const item = project() - if (!item) return - showEditProjectDialog(item) - }} - > - {language.t("common.edit")} - - { - const item = project() - if (!item) return - toggleProjectWorkspaces(item) - }} - > - - {workspacesEnabled() - ? language.t("sidebar.workspaces.disable") - : language.t("sidebar.workspaces.enable")} - - - - - {language.t("sidebar.project.clearNotifications")} - - - - { - const dir = worktree() - if (!dir) return - closeProject(dir) - }} - > - {language.t("common.close")} - - - -
-
-
- + +
+ +
+
+ +
+ + } + > <>
-
- +
+ + + +
{ + if (!panelProps.mobile) scrollContainerRef = el + }} + class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]" + > + + + {(directory) => ( + + )} + + +
+ + store.activeWorkspace} + workspaceLabel={workspaceLabel} + /> + +
- } - > - <> -
- -
-
- - - -
{ - if (!panelProps.mobile) scrollContainerRef = el - }} - class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]" - > - - - {(directory) => ( - - )} - - -
- - store.activeWorkspace} - workspaceLabel={workspaceLabel} - /> - -
-
- - -
- +
+
+ + )}
) - const [loading] = createResource( - () => route()?.store?.[0]?.bootstrapPromise, - (p) => p, - ) - return (
- {(autoselecting(), loading()) ?? ""} + {autoselecting() ?? ""}
diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index c1836fa8a4..0202cfc3be 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -317,12 +317,11 @@ export const SortableWorkspace = (props: { }) const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local())) const boot = createMemo(() => open() || active()) - const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false) const count = createMemo(() => sessions()?.length ?? 0) const hasMore = createMemo(() => workspaceStore.sessionTotal > count()) + const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) })) const busy = createMemo(() => props.ctx.isBusy(props.directory)) - const wasBusy = createMemo((prev) => prev || busy(), false) - const loading = createMemo(() => open() && !booted() && count() === 0 && !wasBusy()) + const loading = () => query.isLoading const touch = createMediaQuery("(hover: none)") const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id))) const loadMore = async () => { @@ -427,7 +426,7 @@ export const SortableWorkspace = (props: { mobile={props.mobile} ctx={props.ctx} showNew={showNew} - loading={loading} + loading={() => query.isLoading && count() === 0} sessions={sessions} hasMore={hasMore} loadMore={loadMore} @@ -453,11 +452,10 @@ export const LocalWorkspace = (props: { }) const slug = createMemo(() => base64Encode(props.project.worktree)) const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow())) - const booted = createMemo((prev) => prev || workspace().store.status === "complete", false) const count = createMemo(() => sessions()?.length ?? 0) const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) })) - const loading = createMemo(() => query.isPending && count() === 0) const hasMore = createMemo(() => workspace().store.sessionTotal > count()) + const loading = () => query.isLoading && count() === 0 const loadMore = async () => { workspace().setStore("limit", (limit) => (limit ?? 0) + 5) await globalSync.project.loadSessions(props.project.worktree) @@ -473,7 +471,7 @@ export const LocalWorkspace = (props: { mobile={props.mobile} ctx={props.ctx} showNew={() => false} - loading={() => query.isLoading} + loading={loading} sessions={sessions} hasMore={hasMore} loadMore={loadMore} diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts index dce0e94c3b..0cac30cb1e 100644 --- a/packages/app/src/utils/persist.ts +++ b/packages/app/src/utils/persist.ts @@ -469,7 +469,7 @@ export function persisted( state, setState, init, - Object.assign(() => ready() === true, { + Object.assign(() => (ready.loading ? false : ready.latest === true), { promise: init instanceof Promise ? init : undefined, }), ] From c57c5315c17e1648d6699abd25db8ee195f44de1 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 07:27:13 +0000 Subject: [PATCH 079/335] chore: generate --- packages/app/src/pages/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 6f6b3c5557..3d3bd5e97b 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -577,7 +577,7 @@ export default function Layout(props: ParentProps) { return projects.find((p) => p.worktree === root) }) - + const [autoselecting] = createResource(async () => { await ready.promise await layout.ready.promise From ec3ac0c4b0a78d0f4de268a1d54401182497fabc Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 17 Apr 2026 14:29:46 +0200 Subject: [PATCH 080/335] upgrade opentui to 0.1.100 (#22928) --- bun.lock | 28 ++++++++++++++-------------- packages/opencode/package.json | 4 ++-- packages/plugin/package.json | 8 ++++---- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/bun.lock b/bun.lock index c22a2ff270..ba6c196241 100644 --- a/bun.lock +++ b/bun.lock @@ -365,8 +365,8 @@ "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "0.1.100", + "@opentui/solid": "0.1.100", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -465,16 +465,16 @@ "zod": "catalog:", }, "devDependencies": { - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "0.1.100", + "@opentui/solid": "0.1.100", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "typescript": "catalog:", }, "peerDependencies": { - "@opentui/core": ">=0.1.99", - "@opentui/solid": ">=0.1.99", + "@opentui/core": ">=0.1.100", + "@opentui/solid": ">=0.1.100", }, "optionalPeers": [ "@opentui/core", @@ -1598,21 +1598,21 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], - "@opentui/core": ["@opentui/core@0.1.99", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.99", "@opentui/core-darwin-x64": "0.1.99", "@opentui/core-linux-arm64": "0.1.99", "@opentui/core-linux-x64": "0.1.99", "@opentui/core-win32-arm64": "0.1.99", "@opentui/core-win32-x64": "0.1.99", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-I3+AEgGzqNWIpWX9g2WOscSPwtQDNOm4KlBjxBWCZjLxkF07u77heWXF7OiAdhKLtNUW6TFiyt6yznqAZPdG3A=="], + "@opentui/core": ["@opentui/core@0.1.100", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.100", "@opentui/core-darwin-x64": "0.1.100", "@opentui/core-linux-arm64": "0.1.100", "@opentui/core-linux-x64": "0.1.100", "@opentui/core-win32-arm64": "0.1.100", "@opentui/core-win32-x64": "0.1.100", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-g6Ft3CcOVpytMzq2AZrnHHeMrmvYeLVCsy/8LqZ30VnPv0zfJ+f1TVi/EFrcl4m0GRPdy6yBOVOMcIAWHSZvtg=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.99", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bzVrqeX2vb5iWrc/ftOUOqeUY8XO+qSgoTwj5TXHuwagavgwD3Hpeyjx8+icnTTeM4pao0som1WR9xfye6/X5Q=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.100", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cY70nNjkh53tys4iQ0FDST3CQfN4Rp8zOrT6EW0dH/51KZq2Rg3EhxDpc+qu4zASadR5uuU5i61g8lYyVGeGgw=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.99", "", { "os": "darwin", "cpu": "x64" }, "sha512-VE4FrXBYpkxnvkqcCV1a8aN9jyyMJMihVW+V2NLCtp+4yQsj0AapG5TiUSN76XnmSZRptxDy5rBmEempeoIZbg=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.100", "", { "os": "darwin", "cpu": "x64" }, "sha512-RUJa4MPX5BWwXuc5nE+Zc+md8+ITYp5X0ourM4+ReaIIW9pTxSDcIPBba9pkJb3PIADyulUPMmmy5OX+umDJhQ=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.99", "", { "os": "linux", "cpu": "arm64" }, "sha512-viXQsbpS7yHjYkl7+am32JdvG96QU9lvHh1UiZtpOxcNUUqiYmA2ZwZFPD2Bi54jNyj5l2hjH6YkD3DzE2FEWA=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.100", "", { "os": "linux", "cpu": "arm64" }, "sha512-Hznr39cSXg+2sQd+WcTsk67BjyqrZvuTK6f94Uu1ULYZJYCE4KdyNU2NzJSK6ooyosSWXPDsb4kwetTlfypMZg=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.99", "", { "os": "linux", "cpu": "x64" }, "sha512-WLoEFINOSp0tZSR9y4LUuGc7n4Y7H1wcpjUPzQ9vChkYDXrfZltEanzoDWbDcQ4kZQW5tHVC7LrZHpAsRLwFZg=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.100", "", { "os": "linux", "cpu": "x64" }, "sha512-mssQIwH2DU1aMGSUnTJgahMeAEfF82n2WKeTGabTRkPrcxD/6ML+JFiV9l8/8YA880fBD2Kh1XXGEhN2ZGB5UA=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.99", "", { "os": "win32", "cpu": "arm64" }, "sha512-yWMOLWCEO8HdrctU1dMkgZC8qGkiO4Dwr4/e11tTvVpRmYhDsP/IR89ZjEEtOwnKwFOFuB/MxvflqaEWVQ2g5Q=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.100", "", { "os": "win32", "cpu": "arm64" }, "sha512-EuLA6+kiIyW/hbo7k56QTc9/QGxg2bYHQ+XPn1UWPf6tmjzbDUwzhw+y52CNJQZpTXtAQSNtxfiYIbigGBd4TQ=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.99", "", { "os": "win32", "cpu": "x64" }, "sha512-aYRlsL2w8YRL6vPd7/hrqlNVkXU3QowWb01TOvAcHS8UAsXaGFUr47kSDyjxDi1wg1MzmVduCfsC7T3NoThV1w=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.100", "", { "os": "win32", "cpu": "x64" }, "sha512-RyiqbKQ15olR8hK4VsPKL3gHrOIIpqEXhUP0jzXJdQQNusmQKlxHyk5EY3R6hi52IgqjjGxwG+ocVeRb4/VTRg=="], - "@opentui/solid": ["@opentui/solid@0.1.99", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.99", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-DrqqO4h2V88FmeIP2cErYkMU0ZK5MrUsZw3w6IzZpoXyyiL4/9qpWzUq+CXx+r16VP2iGxDJwGKUmtFAzUch2Q=="], + "@opentui/solid": ["@opentui/solid@0.1.100", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.100", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-7ADpXOIXtdNzp/r0eQdUuO3kBof7YdcJhQBj/Fv1ckNKoq1XScMuxV2kIK5IzU1Jr3PC4mDxRbek9bMvLJEVjw=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 1dabd91b8d..51e1058839 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -122,8 +122,8 @@ "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "0.1.100", + "@opentui/solid": "0.1.100", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 6f9a0ea1dc..3eb55979ae 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -22,8 +22,8 @@ "zod": "catalog:" }, "peerDependencies": { - "@opentui/core": ">=0.1.99", - "@opentui/solid": ">=0.1.99" + "@opentui/core": ">=0.1.100", + "@opentui/solid": ">=0.1.100" }, "peerDependenciesMeta": { "@opentui/core": { @@ -34,8 +34,8 @@ } }, "devDependencies": { - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "0.1.100", + "@opentui/solid": "0.1.100", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "typescript": "catalog:", From e78d75a003acfdbd9263753913d1bee395ef652a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 13:07:11 +0000 Subject: [PATCH 081/335] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 54fd991eca..c19a837e84 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-OPbZUo/fQv2Xsf+NEZV08GLBMN/DXovhRvn2JkesFtY=", - "aarch64-linux": "sha256-WK7xlVLuirKDN5LaqjBn7qpv5bYVtYHZw0qRNKX4xXg=", - "aarch64-darwin": "sha256-BAoAdeLQ+lXDD7Klxoxij683OVVug8KXEMRUqIQAjc8=", - "x86_64-darwin": "sha256-ZOBwNR2gZgc5f+y3VIBBT4qZpeZfg7Of6AaGDOfqsG8=" + "x86_64-linux": "sha256-yGb+EPlFNDptIi4yFdJ0z7fhAyfOCRXu0GpNxrOnLVA=", + "aarch64-linux": "sha256-h8oBwLB6LnFN4xGB0/ocvxRbBMwewrEck91ADihTUCk=", + "aarch64-darwin": "sha256-/0IOgoihi4OR2AhDDfstLn2DcY/261v/KRQV4Pwhbmk=", + "x86_64-darwin": "sha256-rPkqF+led93s1plBbhFpszrnzF2H+EUz8QlfbUkSHvM=" } } From 06ae43920b3f2384187135cbd71248a8f49ba98f Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 17 Apr 2026 13:37:06 +0000 Subject: [PATCH 082/335] release: v1.4.8 --- bun.lock | 32 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop-electron/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++----- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/shared/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 19 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index ba6c196241..b6f071c460 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.4.7", + "version": "1.4.8", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -83,7 +83,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.4.7", + "version": "1.4.8", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -117,7 +117,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.4.7", + "version": "1.4.8", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -144,7 +144,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.4.7", + "version": "1.4.8", "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/openai": "3.0.48", @@ -168,7 +168,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.4.7", + "version": "1.4.8", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -192,7 +192,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.4.7", + "version": "1.4.8", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -225,7 +225,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "1.4.7", + "version": "1.4.8", "dependencies": { "effect": "catalog:", "electron-context-menu": "4.1.2", @@ -268,7 +268,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.4.7", + "version": "1.4.8", "dependencies": { "@opencode-ai/shared": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -297,7 +297,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.4.7", + "version": "1.4.8", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -313,7 +313,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.4.7", + "version": "1.4.8", "bin": { "opencode": "./bin/opencode", }, @@ -458,7 +458,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.4.7", + "version": "1.4.8", "dependencies": { "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", @@ -493,7 +493,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.4.7", + "version": "1.4.8", "dependencies": { "cross-spawn": "catalog:", }, @@ -508,7 +508,7 @@ }, "packages/shared": { "name": "@opencode-ai/shared", - "version": "1.4.7", + "version": "1.4.8", "bin": { "opencode": "./bin/opencode", }, @@ -532,7 +532,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.4.7", + "version": "1.4.8", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -567,7 +567,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.4.7", + "version": "1.4.8", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -616,7 +616,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.4.7", + "version": "1.4.8", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 2941637d08..56ca71f892 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.4.7", + "version": "1.4.8", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 8783f3fd05..528427ae80 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.4.7", + "version": "1.4.8", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index cdefd0e609..1b4292e9ad 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.4.7", + "version": "1.4.8", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 898c540bac..f3de599f1c 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.4.7", + "version": "1.4.8", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 46ff28b7d1..b13ad8520e 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.4.7", + "version": "1.4.8", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index e1f69b5b20..ed36f62827 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "1.4.7", + "version": "1.4.8", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index d8eea4ea36..ce0ad5d4ab 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.4.7", + "version": "1.4.8", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 12a72e647f..f667de3822 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.4.7", + "version": "1.4.8", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index d164534cf7..31facbe3b5 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.4.7" +version = "1.4.8" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.7/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.8/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.7/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.8/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.7/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.8/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.7/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.8/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.7/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.8/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 36a9ddc321..877c9dcd14 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.4.7", + "version": "1.4.8", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 51e1058839..83d6e526fd 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.4.7", + "version": "1.4.8", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 3eb55979ae..0e51002b42 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.4.7", + "version": "1.4.8", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 53a5893143..06213308f1 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.4.7", + "version": "1.4.8", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/shared/package.json b/packages/shared/package.json index 9dec6bdb6c..794c537da8 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.4.7", + "version": "1.4.8", "name": "@opencode-ai/shared", "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index a23500241e..d9865aa35c 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.4.7", + "version": "1.4.8", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index cd559041cc..f05ce03826 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.4.7", + "version": "1.4.8", "type": "module", "license": "MIT", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index a53ef51932..a1d138319f 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.4.7", + "version": "1.4.8", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index c499f679fe..07a3682c67 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.4.7", + "version": "1.4.8", "publisher": "sst-dev", "repository": { "type": "git", From fffc496f41ffc2be3149dbd2faf9a577be0a390a Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 17 Apr 2026 09:46:35 -0400 Subject: [PATCH 083/335] remove log --- packages/opencode/src/cli/cmd/tui/app.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index f8ffd27dc8..74eca9a0f2 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -135,9 +135,7 @@ export function tui(input: { await TuiPluginRuntime.dispose() } - console.log("starting renderer") const renderer = await createCliRenderer(rendererConfig(input.config)) - console.log("renderer started") await render(() => { return ( From 0f80c827ed2f0ee32f8dfb8812605ed64ab47254 Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 17 Apr 2026 09:52:10 -0400 Subject: [PATCH 084/335] feat(core): exponential backoff of workspace reconnect (#23083) --- .../tui/component/dialog-workspace-create.tsx | 4 + .../opencode/src/control-plane/workspace.ts | 119 ++++++++++-------- 2 files changed, 68 insertions(+), 55 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index ad5cd45782..7ea513edee 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -229,6 +229,10 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) = }) const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch((err) => { + toast.show({ + message: "Creating workspace failed", + variant: "error", + }) log.error("workspace create request failed", { type, error: errorData(err), diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index 3af11707e8..fd22d3af04 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -34,7 +34,6 @@ export type Info = z.infer export const ConnectionStatus = z.object({ workspaceID: WorkspaceID.zod, status: z.enum(["connected", "connecting", "disconnected", "error"]), - error: z.string().optional(), }) export type ConnectionStatus = z.infer @@ -345,10 +344,10 @@ const connections = new Map() const aborts = new Map() const TIMEOUT = 5000 -function setStatus(id: WorkspaceID, status: ConnectionStatus["status"], error?: string) { +function setStatus(id: WorkspaceID, status: ConnectionStatus["status"]) { const prev = connections.get(id) - if (prev?.status === status && prev?.error === error) return - const next = { workspaceID: id, status, error } + if (prev?.status === status) return + const next = { workspaceID: id, status } connections.set(id, next) if (status === "error") { @@ -425,68 +424,78 @@ function route(url: string | URL, path: string) { return next } -async function syncWorkspace(space: Info, signal: AbortSignal) { +async function connectSSE(url: URL | string, headers: HeadersInit | undefined, signal: AbortSignal) { + const res = await fetch(route(url, "/global/event"), { + method: "GET", + headers, + signal, + }) + + if (!res.ok) throw new Error(`Workspace sync HTTP failure: ${res.status}`) + if (!res.body) throw new Error("No response body from global sync") + + return res.body +} + +async function syncWorkspaceLoop(space: Info, signal: AbortSignal) { + const adaptor = await getAdaptor(space.projectID, space.type) + const target = await adaptor.target(space) + + if (target.type === "local") return null + + let attempt = 0 + while (!signal.aborted) { log.info("connecting to global sync", { workspace: space.name }) setStatus(space.id, "connecting") - const adaptor = await getAdaptor(space.projectID, space.type) - const target = await adaptor.target(space) - - if (target.type === "local") return - - const res = await fetch(route(target.url, "/global/event"), { - method: "GET", - headers: target.headers, - signal, - }).catch((err: unknown) => { - setStatus(space.id, "error", err instanceof Error ? err.message : String(err)) - + let stream + try { + stream = await connectSSE(target.url, target.headers, signal) + } catch (err) { + setStatus(space.id, "error") log.info("failed to connect to global sync", { workspace: space.name, - error: err, + err, }) - return undefined - }) - - if (!res || !res.ok || !res.body) { - const error = !res ? "No response from global sync" : `Global sync HTTP ${res.status}` - log.info("failed to connect to global sync", { workspace: space.name, error }) - setStatus(space.id, "error", error) - await sleep(1000) - continue } - log.info("global sync connected", { workspace: space.name }) - setStatus(space.id, "connected") + if (stream) { + attempt = 0 - await parseSSE(res.body, signal, (evt: any) => { - try { - if (!("payload" in evt)) return + log.info("global sync connected", { workspace: space.name }) + setStatus(space.id, "connected") - if (evt.payload.type === "sync") { - SyncEvent.replay(evt.payload.syncEvent as SyncEvent.SerializedEvent) + await parseSSE(stream, signal, (evt: any) => { + try { + if (!("payload" in evt)) return + + if (evt.payload.type === "sync") { + SyncEvent.replay(evt.payload.syncEvent as SyncEvent.SerializedEvent) + } + + GlobalBus.emit("event", { + directory: evt.directory, + project: evt.project, + workspace: space.id, + payload: evt.payload, + }) + } catch (err) { + log.info("failed to replay global event", { + workspaceID: space.id, + error: err, + }) } + }) - GlobalBus.emit("event", { - directory: evt.directory, - project: evt.project, - workspace: space.id, - payload: evt.payload, - }) - } catch (err) { - log.info("failed to replay global event", { - workspaceID: space.id, - error: err, - }) - } - }) + log.info("disconnected from global sync: " + space.id) + setStatus(space.id, "disconnected") + } - log.info("disconnected from global sync: " + space.id) - setStatus(space.id, "disconnected") - - // TODO: Implement exponential backoff - await sleep(1000) + // Back off reconnect attempts up to 2 minutes while the workspace + // stays unavailable. + await sleep(Math.min(120_000, 1_000 * 2 ** attempt)) + attempt += 1 } } @@ -498,7 +507,7 @@ async function startSync(space: Info) { if (target.type === "local") { void Filesystem.exists(target.directory).then((exists) => { - setStatus(space.id, exists ? "connected" : "error", exists ? undefined : "directory does not exist") + setStatus(space.id, exists ? "connected" : "error") }) return } @@ -510,10 +519,10 @@ async function startSync(space: Info) { const abort = new AbortController() aborts.set(space.id, abort) - void syncWorkspace(space, abort.signal).catch((error) => { + void syncWorkspaceLoop(space, abort.signal).catch((error) => { aborts.delete(space.id) - setStatus(space.id, "error", String(error)) + setStatus(space.id, "error") log.warn("workspace listener failed", { workspaceID: space.id, error, From cb425ac927cde685ef9b6e38a62ad71c408a47df Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 13:53:11 +0000 Subject: [PATCH 085/335] chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 2 -- packages/sdk/openapi.json | 6 ------ 2 files changed, 8 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 839dae8b22..460c2bcdfa 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -535,7 +535,6 @@ export type EventWorkspaceStatus = { properties: { workspaceID: string status: "connected" | "connecting" | "disconnected" | "error" - error?: string } } @@ -2490,7 +2489,6 @@ export type ExperimentalWorkspaceStatusResponses = { 200: Array<{ workspaceID: string status: "connected" | "connecting" | "disconnected" | "error" - error?: string }> } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index cf14026eae..7bdf025bbe 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -639,9 +639,6 @@ "status": { "type": "string", "enum": ["connected", "connecting", "disconnected", "error"] - }, - "error": { - "type": "string" } }, "required": ["workspaceID", "status"] @@ -8852,9 +8849,6 @@ "status": { "type": "string", "enum": ["connected", "connecting", "disconnected", "error"] - }, - "error": { - "type": "string" } }, "required": ["workspaceID", "status"] From 3707e4a49cb97639408a9e0da7cf148ca5ce8834 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 17 Apr 2026 09:54:44 -0400 Subject: [PATCH 086/335] zen: routing logic --- .../app/src/routes/zen/util/handler.ts | 24 +- .../src/routes/zen/util/modelTpmLimiter.ts | 49 + .../migration.sql | 6 + .../snapshot.json | 2615 +++++++++++++++++ packages/console/core/src/model.ts | 15 +- packages/console/core/src/schema/ip.sql.ts | 10 + packages/shared/sst-env.d.ts | 10 + 7 files changed, 2723 insertions(+), 6 deletions(-) create mode 100644 packages/console/app/src/routes/zen/util/modelTpmLimiter.ts create mode 100644 packages/console/core/migrations/20260417071612_tidy_diamondback/migration.sql create mode 100644 packages/console/core/migrations/20260417071612_tidy_diamondback/snapshot.json create mode 100644 packages/shared/sst-env.d.ts diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index d1c5985a81..2e576eaf68 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -45,6 +45,7 @@ import { LiteData } from "@opencode-ai/console-core/lite.js" import { Resource } from "@opencode-ai/console-resource" import { i18n, type Key } from "~/i18n" import { localeFromRequest } from "~/lib/language" +import { createModelTpmLimiter } from "./modelTpmLimiter" type ZenData = Awaited> type RetryOptions = { @@ -121,6 +122,8 @@ export async function handler( const authInfo = await authenticate(modelInfo, zenApiKey) const billingSource = validateBilling(authInfo, modelInfo) logger.metric({ source: billingSource }) + const modelTpmLimiter = createModelTpmLimiter(modelInfo.providers) + const modelTpmLimits = await modelTpmLimiter?.check() const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => { const providerInfo = selectProvider( @@ -133,6 +136,7 @@ export async function handler( trialProviders, retry, stickyProvider, + modelTpmLimits, ) validateModelSettings(billingSource, authInfo) updateProviderKey(authInfo, providerInfo) @@ -229,6 +233,7 @@ export async function handler( const usageInfo = providerInfo.normalizeUsage(json.usage) const costInfo = calculateCost(modelInfo, usageInfo) await trialLimiter?.track(usageInfo) + await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo) await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo) await reload(billingSource, authInfo, costInfo) json.cost = calculateOccurredCost(billingSource, costInfo) @@ -278,6 +283,7 @@ export async function handler( const usageInfo = providerInfo.normalizeUsage(usage) const costInfo = calculateCost(modelInfo, usageInfo) await trialLimiter?.track(usageInfo) + await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo) await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo) await reload(billingSource, authInfo, costInfo) const cost = calculateOccurredCost(billingSource, costInfo) @@ -433,12 +439,16 @@ export async function handler( trialProviders: string[] | undefined, retry: RetryOptions, stickyProvider: string | undefined, + modelTpmLimits: Record | undefined, ) { const modelProvider = (() => { + // Byok is top priority b/c if user set their own API key, we should use it + // instead of using the sticky provider for the same session if (authInfo?.provider?.credentials) { return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider) } + // Always use the same provider for the same session if (stickyProvider) { const provider = modelInfo.providers.find((provider) => provider.id === stickyProvider) if (provider) return provider @@ -451,10 +461,20 @@ export async function handler( } if (retry.retryCount !== MAX_FAILOVER_RETRIES) { - const providers = modelInfo.providers + const allProviders = modelInfo.providers .filter((provider) => !provider.disabled) + .filter((provider) => provider.weight !== 0) .filter((provider) => !retry.excludeProviders.includes(provider.id)) - .flatMap((provider) => Array(provider.weight ?? 1).fill(provider)) + .filter((provider) => { + if (!provider.tpmLimit) return true + const usage = modelTpmLimits?.[`${provider.id}/${provider.model}`] ?? 0 + return usage < provider.tpmLimit * 1_000_000 + }) + + const topPriority = Math.min(...allProviders.map((p) => p.priority)) + const providers = allProviders + .filter((p) => p.priority <= topPriority) + .flatMap((provider) => Array(provider.weight).fill(provider)) // Use the last 4 characters of session ID to select a provider const identifier = sessionId.length ? sessionId : ip diff --git a/packages/console/app/src/routes/zen/util/modelTpmLimiter.ts b/packages/console/app/src/routes/zen/util/modelTpmLimiter.ts new file mode 100644 index 0000000000..eeb89da5f2 --- /dev/null +++ b/packages/console/app/src/routes/zen/util/modelTpmLimiter.ts @@ -0,0 +1,49 @@ +import { and, Database, eq, inArray, sql } from "@opencode-ai/console-core/drizzle/index.js" +import { ModelRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js" +import { UsageInfo } from "./provider/provider" + +export function createModelTpmLimiter(providers: { id: string; model: string; tpmLimit?: number }[]) { + const keys = providers.filter((p) => p.tpmLimit).map((p) => `${p.id}/${p.model}`) + if (keys.length === 0) return + + const yyyyMMddHHmm = new Date(Date.now()) + .toISOString() + .replace(/[^0-9]/g, "") + .substring(0, 12) + + return { + check: async () => { + const data = await Database.use((tx) => + tx + .select() + .from(ModelRateLimitTable) + .where(and(inArray(ModelRateLimitTable.key, keys), eq(ModelRateLimitTable.interval, yyyyMMddHHmm))), + ) + + // convert to map of model to count + return data.reduce( + (acc, curr) => { + acc[curr.key] = curr.count + return acc + }, + {} as Record, + ) + }, + track: async (id: string, model: string, usageInfo: UsageInfo) => { + const usage = + usageInfo.inputTokens + + usageInfo.outputTokens + + (usageInfo.reasoningTokens ?? 0) + + (usageInfo.cacheReadTokens ?? 0) + + (usageInfo.cacheWrite5mTokens ?? 0) + + (usageInfo.cacheWrite1hTokens ?? 0) + if (usage <= 0) return + await Database.use((tx) => + tx + .insert(ModelRateLimitTable) + .values({ key: `${id}/${model}`, interval: yyyyMMddHHmm, count: usage }) + .onDuplicateKeyUpdate({ set: { count: sql`${ModelRateLimitTable.count} + ${usage}` } }), + ) + }, + } +} diff --git a/packages/console/core/migrations/20260417071612_tidy_diamondback/migration.sql b/packages/console/core/migrations/20260417071612_tidy_diamondback/migration.sql new file mode 100644 index 0000000000..41a4efe68e --- /dev/null +++ b/packages/console/core/migrations/20260417071612_tidy_diamondback/migration.sql @@ -0,0 +1,6 @@ +CREATE TABLE `model_rate_limit` ( + `key` varchar(255) NOT NULL, + `interval` varchar(40) NOT NULL, + `count` int NOT NULL, + CONSTRAINT PRIMARY KEY(`key`,`interval`) +); diff --git a/packages/console/core/migrations/20260417071612_tidy_diamondback/snapshot.json b/packages/console/core/migrations/20260417071612_tidy_diamondback/snapshot.json new file mode 100644 index 0000000000..169d7dbb4f --- /dev/null +++ b/packages/console/core/migrations/20260417071612_tidy_diamondback/snapshot.json @@ -0,0 +1,2615 @@ +{ + "version": "6", + "dialect": "mysql", + "id": "93c492af-c95b-4213-9fc2-38c3dd10374d", + "prevIds": [ + "a09a925d-6cdd-4e7c-b8b1-11c259928b4c" + ], + "ddl": [ + { + "name": "account", + "entityType": "tables" + }, + { + "name": "auth", + "entityType": "tables" + }, + { + "name": "benchmark", + "entityType": "tables" + }, + { + "name": "billing", + "entityType": "tables" + }, + { + "name": "lite", + "entityType": "tables" + }, + { + "name": "payment", + "entityType": "tables" + }, + { + "name": "subscription", + "entityType": "tables" + }, + { + "name": "usage", + "entityType": "tables" + }, + { + "name": "ip_rate_limit", + "entityType": "tables" + }, + { + "name": "ip", + "entityType": "tables" + }, + { + "name": "key_rate_limit", + "entityType": "tables" + }, + { + "name": "model_rate_limit", + "entityType": "tables" + }, + { + "name": "key", + "entityType": "tables" + }, + { + "name": "model", + "entityType": "tables" + }, + { + "name": "provider", + "entityType": "tables" + }, + { + "name": "user", + "entityType": "tables" + }, + { + "name": "workspace", + "entityType": "tables" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "account" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "auth" + }, + { + "type": "enum('email','github','google')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subject", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "account_id", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "mediumtext", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "result", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "customer_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(32)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_type", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(4)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_last4", + "entityType": "columns", + "table": "billing" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "balance", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_limit", + "entityType": "columns", + "table": "billing" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_usage_updated", + "entityType": "columns", + "table": "billing" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_trigger", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_amount", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_error", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_reload_error", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_reload_locked_till", + "entityType": "columns", + "table": "billing" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(28)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "enum('20','100','200')", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription_plan", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_subscription_booked", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_subscription_selected", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(28)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "lite_subscription_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "lite", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "rolling_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "weekly_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_rolling_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_weekly_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "customer_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "invoice_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "amount", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_refunded", + "entityType": "columns", + "table": "payment" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "enrichment", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "rolling_usage", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "fixed_usage", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_rolling_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_fixed_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "input_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "output_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reasoning_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_read_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_write_5m_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_write_1h_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "enrichment", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(45)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "ip", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "varchar(10)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "count", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "varchar(45)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "ip", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "ip" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "usage", + "entityType": "columns", + "table": "ip" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key", + "entityType": "columns", + "table": "key_rate_limit" + }, + { + "type": "varchar(40)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "key_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "count", + "entityType": "columns", + "table": "key_rate_limit" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key", + "entityType": "columns", + "table": "model_rate_limit" + }, + { + "type": "varchar(40)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "model_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "count", + "entityType": "columns", + "table": "model_rate_limit" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "provider" + }, + { + "type": "text", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "credentials", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "account_id", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_seen", + "entityType": "columns", + "table": "user" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "color", + "entityType": "columns", + "table": "user" + }, + { + "type": "enum('admin','member')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "role", + "entityType": "columns", + "table": "user" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_limit", + "entityType": "columns", + "table": "user" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_usage_updated", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "workspace" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "auth", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "benchmark", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "billing", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "lite", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "payment", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "subscription", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "usage", + "entityType": "pks" + }, + { + "columns": [ + "ip", + "interval" + ], + "name": "PRIMARY", + "table": "ip_rate_limit", + "entityType": "pks" + }, + { + "columns": [ + "ip" + ], + "name": "PRIMARY", + "table": "ip", + "entityType": "pks" + }, + { + "columns": [ + "key", + "interval" + ], + "name": "PRIMARY", + "table": "key_rate_limit", + "entityType": "pks" + }, + { + "columns": [ + "key", + "interval" + ], + "name": "PRIMARY", + "table": "model_rate_limit", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "key", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "model", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "provider", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "user", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "provider", + "isExpression": false + }, + { + "value": "subject", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "provider", + "entityType": "indexes", + "table": "auth" + }, + { + "columns": [ + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "account_id", + "entityType": "indexes", + "table": "auth" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "time_created", + "entityType": "indexes", + "table": "benchmark" + }, + { + "columns": [ + { + "value": "customer_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_customer_id", + "entityType": "indexes", + "table": "billing" + }, + { + "columns": [ + { + "value": "subscription_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_subscription_id", + "entityType": "indexes", + "table": "billing" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_user_id", + "entityType": "indexes", + "table": "lite" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_user_id", + "entityType": "indexes", + "table": "subscription" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "usage_time_created", + "entityType": "indexes", + "table": "usage" + }, + { + "columns": [ + { + "value": "key", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_key", + "entityType": "indexes", + "table": "key" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "model", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "model_workspace_model", + "entityType": "indexes", + "table": "model" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "provider", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_provider", + "entityType": "indexes", + "table": "provider" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "user_account_id", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "email", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "user_email", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_account_id", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "email", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_email", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "slug", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "slug", + "entityType": "indexes", + "table": "workspace" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/console/core/src/model.ts b/packages/console/core/src/model.ts index 3d614d3034..6281382d65 100644 --- a/packages/console/core/src/model.ts +++ b/packages/console/core/src/model.ts @@ -34,6 +34,8 @@ export namespace ZenData { z.object({ id: z.string(), model: z.string(), + priority: z.number().optional(), + tpmLimit: z.number().optional(), weight: z.number().optional(), disabled: z.boolean().optional(), storeModel: z.string().optional(), @@ -123,10 +125,16 @@ export namespace ZenData { ), models: (() => { const normalize = (model: z.infer) => { - const composite = model.providers.find((p) => compositeProviders[p.id].length > 1) + const providers = model.providers.map((p) => ({ + ...p, + priority: p.priority ?? Infinity, + weight: p.weight ?? 1, + })) + const composite = providers.find((p) => compositeProviders[p.id].length > 1) if (!composite) return { trialProvider: model.trialProvider ? [model.trialProvider] : undefined, + providers, } const weightMulti = compositeProviders[composite.id].length @@ -137,17 +145,16 @@ export namespace ZenData { if (model.trialProvider === composite.id) return compositeProviders[composite.id].map((p) => p.id) return [model.trialProvider] })(), - providers: model.providers.flatMap((p) => + providers: providers.flatMap((p) => p.id === composite.id ? compositeProviders[p.id].map((sub) => ({ ...p, id: sub.id, - weight: p.weight ?? 1, })) : [ { ...p, - weight: (p.weight ?? 1) * weightMulti, + weight: p.weight * weightMulti, }, ], ), diff --git a/packages/console/core/src/schema/ip.sql.ts b/packages/console/core/src/schema/ip.sql.ts index a840a78c19..830842e64d 100644 --- a/packages/console/core/src/schema/ip.sql.ts +++ b/packages/console/core/src/schema/ip.sql.ts @@ -30,3 +30,13 @@ export const KeyRateLimitTable = mysqlTable( }, (table) => [primaryKey({ columns: [table.key, table.interval] })], ) + +export const ModelRateLimitTable = mysqlTable( + "model_rate_limit", + { + key: varchar("key", { length: 255 }).notNull(), + interval: varchar("interval", { length: 40 }).notNull(), + count: int("count").notNull(), + }, + (table) => [primaryKey({ columns: [table.key, table.interval] })], +) diff --git a/packages/shared/sst-env.d.ts b/packages/shared/sst-env.d.ts new file mode 100644 index 0000000000..64441936d7 --- /dev/null +++ b/packages/shared/sst-env.d.ts @@ -0,0 +1,10 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +/* deno-fmt-ignore-file */ +/* biome-ignore-all lint: auto-generated */ + +/// + +import "sst" +export {} \ No newline at end of file From cc063d4c32a9d9800a27b4e73fc46781e2c2f08b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 13:56:17 +0000 Subject: [PATCH 087/335] chore: generate --- .../snapshot.json | 86 ++++--------------- 1 file changed, 19 insertions(+), 67 deletions(-) diff --git a/packages/console/core/migrations/20260417071612_tidy_diamondback/snapshot.json b/packages/console/core/migrations/20260417071612_tidy_diamondback/snapshot.json index 169d7dbb4f..2152bfa76f 100644 --- a/packages/console/core/migrations/20260417071612_tidy_diamondback/snapshot.json +++ b/packages/console/core/migrations/20260417071612_tidy_diamondback/snapshot.json @@ -2,9 +2,7 @@ "version": "6", "dialect": "mysql", "id": "93c492af-c95b-4213-9fc2-38c3dd10374d", - "prevIds": [ - "a09a925d-6cdd-4e7c-b8b1-11c259928b4c" - ], + "prevIds": ["a09a925d-6cdd-4e7c-b8b1-11c259928b4c"], "ddl": [ { "name": "account", @@ -2175,149 +2173,103 @@ "table": "workspace" }, { - "columns": [ - "id" - ], + "columns": ["id"], "name": "PRIMARY", "table": "account", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "name": "PRIMARY", "table": "auth", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "name": "PRIMARY", "table": "benchmark", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "billing", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "lite", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "payment", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "subscription", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "usage", "entityType": "pks" }, { - "columns": [ - "ip", - "interval" - ], + "columns": ["ip", "interval"], "name": "PRIMARY", "table": "ip_rate_limit", "entityType": "pks" }, { - "columns": [ - "ip" - ], + "columns": ["ip"], "name": "PRIMARY", "table": "ip", "entityType": "pks" }, { - "columns": [ - "key", - "interval" - ], + "columns": ["key", "interval"], "name": "PRIMARY", "table": "key_rate_limit", "entityType": "pks" }, { - "columns": [ - "key", - "interval" - ], + "columns": ["key", "interval"], "name": "PRIMARY", "table": "model_rate_limit", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "key", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "model", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "provider", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "user", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "name": "PRIMARY", "table": "workspace", "entityType": "pks" @@ -2612,4 +2564,4 @@ } ], "renames": [] -} \ No newline at end of file +} From 7e39c9b95001ce93d2f3124fe12fb307ea5427be Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 17 Apr 2026 10:43:17 -0400 Subject: [PATCH 088/335] back to opentui 0.1.99 --- bun.lock | 26 ++++++++++++++++++++++++-- packages/opencode/package.json | 4 ++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index b6f071c460..e7154c3d1b 100644 --- a/bun.lock +++ b/bun.lock @@ -365,8 +365,8 @@ "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", - "@opentui/core": "0.1.100", - "@opentui/solid": "0.1.100", + "@opentui/core": "0.1.99", + "@opentui/solid": "0.1.99", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -5926,6 +5926,10 @@ "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], + "opencode/@opentui/core": ["@opentui/core@0.1.99", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.99", "@opentui/core-darwin-x64": "0.1.99", "@opentui/core-linux-arm64": "0.1.99", "@opentui/core-linux-x64": "0.1.99", "@opentui/core-win32-arm64": "0.1.99", "@opentui/core-win32-x64": "0.1.99", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-I3+AEgGzqNWIpWX9g2WOscSPwtQDNOm4KlBjxBWCZjLxkF07u77heWXF7OiAdhKLtNUW6TFiyt6yznqAZPdG3A=="], + + "opencode/@opentui/solid": ["@opentui/solid@0.1.99", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.99", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-DrqqO4h2V88FmeIP2cErYkMU0ZK5MrUsZw3w6IzZpoXyyiL4/9qpWzUq+CXx+r16VP2iGxDJwGKUmtFAzUch2Q=="], + "opencode/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], "opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], @@ -6690,6 +6694,22 @@ "opencode-poe-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "opencode/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.99", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bzVrqeX2vb5iWrc/ftOUOqeUY8XO+qSgoTwj5TXHuwagavgwD3Hpeyjx8+icnTTeM4pao0som1WR9xfye6/X5Q=="], + + "opencode/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.99", "", { "os": "darwin", "cpu": "x64" }, "sha512-VE4FrXBYpkxnvkqcCV1a8aN9jyyMJMihVW+V2NLCtp+4yQsj0AapG5TiUSN76XnmSZRptxDy5rBmEempeoIZbg=="], + + "opencode/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.99", "", { "os": "linux", "cpu": "arm64" }, "sha512-viXQsbpS7yHjYkl7+am32JdvG96QU9lvHh1UiZtpOxcNUUqiYmA2ZwZFPD2Bi54jNyj5l2hjH6YkD3DzE2FEWA=="], + + "opencode/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.99", "", { "os": "linux", "cpu": "x64" }, "sha512-WLoEFINOSp0tZSR9y4LUuGc7n4Y7H1wcpjUPzQ9vChkYDXrfZltEanzoDWbDcQ4kZQW5tHVC7LrZHpAsRLwFZg=="], + + "opencode/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.99", "", { "os": "win32", "cpu": "arm64" }, "sha512-yWMOLWCEO8HdrctU1dMkgZC8qGkiO4Dwr4/e11tTvVpRmYhDsP/IR89ZjEEtOwnKwFOFuB/MxvflqaEWVQ2g5Q=="], + + "opencode/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.99", "", { "os": "win32", "cpu": "x64" }, "sha512-aYRlsL2w8YRL6vPd7/hrqlNVkXU3QowWb01TOvAcHS8UAsXaGFUr47kSDyjxDi1wg1MzmVduCfsC7T3NoThV1w=="], + + "opencode/@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], + + "opencode/@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="], + "opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "opencontrol/@modelcontextprotocol/sdk/express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], @@ -7022,6 +7042,8 @@ "js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "opencode/@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 83d6e526fd..6edc8bd2bb 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -122,8 +122,8 @@ "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", - "@opentui/core": "0.1.100", - "@opentui/solid": "0.1.100", + "@opentui/core": "0.1.99", + "@opentui/solid": "0.1.99", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", From 10c4ab9a3d63dc55ecba89985c3fd23517e769fd Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 17 Apr 2026 10:51:02 -0400 Subject: [PATCH 089/335] roll back opentui --- bun.lock | 48 ++++++++++------------------------ package.json | 2 ++ packages/opencode/package.json | 4 +-- packages/plugin/package.json | 4 +-- 4 files changed, 20 insertions(+), 38 deletions(-) diff --git a/bun.lock b/bun.lock index e7154c3d1b..776c5fb81e 100644 --- a/bun.lock +++ b/bun.lock @@ -365,8 +365,8 @@ "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "catalog:", + "@opentui/solid": "catalog:", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -465,8 +465,8 @@ "zod": "catalog:", }, "devDependencies": { - "@opentui/core": "0.1.100", - "@opentui/solid": "0.1.100", + "@opentui/core": "catalog:", + "@opentui/solid": "catalog:", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", @@ -675,6 +675,8 @@ "@npmcli/arborist": "9.4.0", "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", + "@opentui/core": "0.1.99", + "@opentui/solid": "0.1.99", "@pierre/diffs": "1.1.0-beta.18", "@playwright/test": "1.59.1", "@solid-primitives/storage": "4.3.3", @@ -1598,21 +1600,21 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], - "@opentui/core": ["@opentui/core@0.1.100", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.100", "@opentui/core-darwin-x64": "0.1.100", "@opentui/core-linux-arm64": "0.1.100", "@opentui/core-linux-x64": "0.1.100", "@opentui/core-win32-arm64": "0.1.100", "@opentui/core-win32-x64": "0.1.100", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-g6Ft3CcOVpytMzq2AZrnHHeMrmvYeLVCsy/8LqZ30VnPv0zfJ+f1TVi/EFrcl4m0GRPdy6yBOVOMcIAWHSZvtg=="], + "@opentui/core": ["@opentui/core@0.1.99", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.99", "@opentui/core-darwin-x64": "0.1.99", "@opentui/core-linux-arm64": "0.1.99", "@opentui/core-linux-x64": "0.1.99", "@opentui/core-win32-arm64": "0.1.99", "@opentui/core-win32-x64": "0.1.99", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-I3+AEgGzqNWIpWX9g2WOscSPwtQDNOm4KlBjxBWCZjLxkF07u77heWXF7OiAdhKLtNUW6TFiyt6yznqAZPdG3A=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.100", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cY70nNjkh53tys4iQ0FDST3CQfN4Rp8zOrT6EW0dH/51KZq2Rg3EhxDpc+qu4zASadR5uuU5i61g8lYyVGeGgw=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.99", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bzVrqeX2vb5iWrc/ftOUOqeUY8XO+qSgoTwj5TXHuwagavgwD3Hpeyjx8+icnTTeM4pao0som1WR9xfye6/X5Q=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.100", "", { "os": "darwin", "cpu": "x64" }, "sha512-RUJa4MPX5BWwXuc5nE+Zc+md8+ITYp5X0ourM4+ReaIIW9pTxSDcIPBba9pkJb3PIADyulUPMmmy5OX+umDJhQ=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.99", "", { "os": "darwin", "cpu": "x64" }, "sha512-VE4FrXBYpkxnvkqcCV1a8aN9jyyMJMihVW+V2NLCtp+4yQsj0AapG5TiUSN76XnmSZRptxDy5rBmEempeoIZbg=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.100", "", { "os": "linux", "cpu": "arm64" }, "sha512-Hznr39cSXg+2sQd+WcTsk67BjyqrZvuTK6f94Uu1ULYZJYCE4KdyNU2NzJSK6ooyosSWXPDsb4kwetTlfypMZg=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.99", "", { "os": "linux", "cpu": "arm64" }, "sha512-viXQsbpS7yHjYkl7+am32JdvG96QU9lvHh1UiZtpOxcNUUqiYmA2ZwZFPD2Bi54jNyj5l2hjH6YkD3DzE2FEWA=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.100", "", { "os": "linux", "cpu": "x64" }, "sha512-mssQIwH2DU1aMGSUnTJgahMeAEfF82n2WKeTGabTRkPrcxD/6ML+JFiV9l8/8YA880fBD2Kh1XXGEhN2ZGB5UA=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.99", "", { "os": "linux", "cpu": "x64" }, "sha512-WLoEFINOSp0tZSR9y4LUuGc7n4Y7H1wcpjUPzQ9vChkYDXrfZltEanzoDWbDcQ4kZQW5tHVC7LrZHpAsRLwFZg=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.100", "", { "os": "win32", "cpu": "arm64" }, "sha512-EuLA6+kiIyW/hbo7k56QTc9/QGxg2bYHQ+XPn1UWPf6tmjzbDUwzhw+y52CNJQZpTXtAQSNtxfiYIbigGBd4TQ=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.99", "", { "os": "win32", "cpu": "arm64" }, "sha512-yWMOLWCEO8HdrctU1dMkgZC8qGkiO4Dwr4/e11tTvVpRmYhDsP/IR89ZjEEtOwnKwFOFuB/MxvflqaEWVQ2g5Q=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.100", "", { "os": "win32", "cpu": "x64" }, "sha512-RyiqbKQ15olR8hK4VsPKL3gHrOIIpqEXhUP0jzXJdQQNusmQKlxHyk5EY3R6hi52IgqjjGxwG+ocVeRb4/VTRg=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.99", "", { "os": "win32", "cpu": "x64" }, "sha512-aYRlsL2w8YRL6vPd7/hrqlNVkXU3QowWb01TOvAcHS8UAsXaGFUr47kSDyjxDi1wg1MzmVduCfsC7T3NoThV1w=="], - "@opentui/solid": ["@opentui/solid@0.1.100", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.100", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-7ADpXOIXtdNzp/r0eQdUuO3kBof7YdcJhQBj/Fv1ckNKoq1XScMuxV2kIK5IzU1Jr3PC4mDxRbek9bMvLJEVjw=="], + "@opentui/solid": ["@opentui/solid@0.1.99", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.99", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-DrqqO4h2V88FmeIP2cErYkMU0ZK5MrUsZw3w6IzZpoXyyiL4/9qpWzUq+CXx+r16VP2iGxDJwGKUmtFAzUch2Q=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], @@ -5926,10 +5928,6 @@ "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], - "opencode/@opentui/core": ["@opentui/core@0.1.99", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.99", "@opentui/core-darwin-x64": "0.1.99", "@opentui/core-linux-arm64": "0.1.99", "@opentui/core-linux-x64": "0.1.99", "@opentui/core-win32-arm64": "0.1.99", "@opentui/core-win32-x64": "0.1.99", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-I3+AEgGzqNWIpWX9g2WOscSPwtQDNOm4KlBjxBWCZjLxkF07u77heWXF7OiAdhKLtNUW6TFiyt6yznqAZPdG3A=="], - - "opencode/@opentui/solid": ["@opentui/solid@0.1.99", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.99", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-DrqqO4h2V88FmeIP2cErYkMU0ZK5MrUsZw3w6IzZpoXyyiL4/9qpWzUq+CXx+r16VP2iGxDJwGKUmtFAzUch2Q=="], - "opencode/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], "opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], @@ -6694,22 +6692,6 @@ "opencode-poe-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - "opencode/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.99", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bzVrqeX2vb5iWrc/ftOUOqeUY8XO+qSgoTwj5TXHuwagavgwD3Hpeyjx8+icnTTeM4pao0som1WR9xfye6/X5Q=="], - - "opencode/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.99", "", { "os": "darwin", "cpu": "x64" }, "sha512-VE4FrXBYpkxnvkqcCV1a8aN9jyyMJMihVW+V2NLCtp+4yQsj0AapG5TiUSN76XnmSZRptxDy5rBmEempeoIZbg=="], - - "opencode/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.99", "", { "os": "linux", "cpu": "arm64" }, "sha512-viXQsbpS7yHjYkl7+am32JdvG96QU9lvHh1UiZtpOxcNUUqiYmA2ZwZFPD2Bi54jNyj5l2hjH6YkD3DzE2FEWA=="], - - "opencode/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.99", "", { "os": "linux", "cpu": "x64" }, "sha512-WLoEFINOSp0tZSR9y4LUuGc7n4Y7H1wcpjUPzQ9vChkYDXrfZltEanzoDWbDcQ4kZQW5tHVC7LrZHpAsRLwFZg=="], - - "opencode/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.99", "", { "os": "win32", "cpu": "arm64" }, "sha512-yWMOLWCEO8HdrctU1dMkgZC8qGkiO4Dwr4/e11tTvVpRmYhDsP/IR89ZjEEtOwnKwFOFuB/MxvflqaEWVQ2g5Q=="], - - "opencode/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.99", "", { "os": "win32", "cpu": "x64" }, "sha512-aYRlsL2w8YRL6vPd7/hrqlNVkXU3QowWb01TOvAcHS8UAsXaGFUr47kSDyjxDi1wg1MzmVduCfsC7T3NoThV1w=="], - - "opencode/@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], - - "opencode/@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="], - "opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "opencontrol/@modelcontextprotocol/sdk/express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], @@ -7042,8 +7024,6 @@ "js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "opencode/@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], diff --git a/package.json b/package.json index 5fecc09922..f1ad03afc8 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,8 @@ "@types/cross-spawn": "6.0.6", "@octokit/rest": "22.0.0", "@hono/zod-validator": "0.4.2", + "@opentui/core": "0.1.99", + "@opentui/solid": "0.1.99", "ulid": "3.0.1", "@kobalte/core": "0.13.11", "@types/luxon": "3.7.1", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 6edc8bd2bb..7d9bfaccdd 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -122,8 +122,8 @@ "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "catalog:", + "@opentui/solid": "catalog:", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 0e51002b42..d4958fa453 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -34,8 +34,8 @@ } }, "devDependencies": { - "@opentui/core": "0.1.100", - "@opentui/solid": "0.1.100", + "@opentui/core": "catalog:", + "@opentui/solid": "catalog:", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "typescript": "catalog:", From 20103eb97be978deec529b718e15678875462098 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 17 Apr 2026 10:53:43 -0400 Subject: [PATCH 090/335] sync --- packages/console/app/src/routes/zen/util/modelTpmLimiter.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/console/app/src/routes/zen/util/modelTpmLimiter.ts b/packages/console/app/src/routes/zen/util/modelTpmLimiter.ts index eeb89da5f2..9a834a1a5b 100644 --- a/packages/console/app/src/routes/zen/util/modelTpmLimiter.ts +++ b/packages/console/app/src/routes/zen/util/modelTpmLimiter.ts @@ -30,6 +30,8 @@ export function createModelTpmLimiter(providers: { id: string; model: string; tp ) }, track: async (id: string, model: string, usageInfo: UsageInfo) => { + const key = `${id}/${model}` + if (!keys.includes(key)) return const usage = usageInfo.inputTokens + usageInfo.outputTokens + @@ -41,7 +43,7 @@ export function createModelTpmLimiter(providers: { id: string; model: string; tp await Database.use((tx) => tx .insert(ModelRateLimitTable) - .values({ key: `${id}/${model}`, interval: yyyyMMddHHmm, count: usage }) + .values({ key, interval: yyyyMMddHHmm, count: usage }) .onDuplicateKeyUpdate({ set: { count: sql`${ModelRateLimitTable.count} + ${usage}` } }), ) }, From 2415820ecdacf1c8a7c94572297515036207238e Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Fri, 17 Apr 2026 23:13:59 +0800 Subject: [PATCH 091/335] fix: conditionally show file tree in beta channel (#23099) --- packages/app/src/pages/session/session-side-panel.tsx | 7 ++++++- packages/app/src/pages/session/use-session-commands.tsx | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 06cbec48b5..99197f0a70 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -52,7 +52,12 @@ export function SessionSidePanel(props: { const { sessionKey, tabs, view } = useSessionLayout() const isDesktop = createMediaQuery("(min-width: 768px)") - const shown = createMemo(() => platform.platform !== "desktop" || settings.general.showFileTree()) + const shown = createMemo( + () => + platform.platform !== "desktop" || + import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" || + settings.general.showFileTree(), + ) const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) const fileOpen = createMemo(() => isDesktop() && shown() && layout.fileTree.opened()) diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index 9bbeb10bde..d649aeb0cb 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -70,7 +70,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => { }) const activeFileTab = tabState.activeFileTab const closableTab = tabState.closableTab - const shown = () => platform.platform !== "desktop" || settings.general.showFileTree() + const shown = () => + platform.platform !== "desktop" || + import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" || + settings.general.showFileTree() const idle = { type: "idle" as const } const status = () => sync.data.session_status[params.id ?? ""] ?? idle From 8fbbca5f4bf6a7a971ce49d7ac2c8122767f5308 Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:25:12 +0200 Subject: [PATCH 092/335] fix(opencode): rescrict github copilot opus 4.7 variants to "medium" (#23097) --- packages/opencode/src/provider/transform.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 0ebd8bbf59..284fb0fcad 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -587,6 +587,12 @@ export function variants(model: Provider.Model): Record [effort, { reasoningEffort: effort }])) + } + } + if (adaptiveEfforts) { return Object.fromEntries( adaptiveEfforts.map((effort) => [ From ac5b395c5d709378592ea6b3ab8c9fe5061b53d5 Mon Sep 17 00:00:00 2001 From: Jen Person Date: Fri, 17 Apr 2026 17:25:42 +0200 Subject: [PATCH 093/335] docs: adding Mistral to docs as a provider (it is already a provider, just docs update) #23070 (#23072) --- packages/web/src/content/docs/providers.mdx | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index bd7e10f928..84e60b6ef3 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -1316,6 +1316,33 @@ To use Kimi K2 from Moonshot AI: --- +### Mistral AI + +1. Head over to the [Mistral AI console](https://console.mistral.ai/), create an account, and generate an API key. + +2. Run the `/connect` command and search for **Mistral AI**. + + ```txt + /connect + ``` + +3. Enter your Mistral API key. + + ```txt + ┌ API key + │ + │ + └ enter + ``` + +4. Run the `/models` command to select a model like _Mistral Medium_. + + ```txt + /models + ``` + +--- + ### Nebius Token Factory 1. Head over to the [Nebius Token Factory console](https://tokenfactory.nebius.com/), create an account, and click **Add Key**. From 3a4b49095c5273383ea581dfbc31628d80ac43a2 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 15:26:45 +0000 Subject: [PATCH 094/335] chore: generate --- packages/web/src/content/docs/providers.mdx | 30 ++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index 84e60b6ef3..163dcdcf2b 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -1320,26 +1320,26 @@ To use Kimi K2 from Moonshot AI: 1. Head over to the [Mistral AI console](https://console.mistral.ai/), create an account, and generate an API key. -2. Run the `/connect` command and search for **Mistral AI**. +2. Run the `/connect` command and search for **Mistral AI**. - ```txt - /connect - ``` + ```txt + /connect + ``` -3. Enter your Mistral API key. +3. Enter your Mistral API key. - ```txt - ┌ API key - │ - │ - └ enter - ``` + ```txt + ┌ API key + │ + │ + └ enter + ``` -4. Run the `/models` command to select a model like _Mistral Medium_. +4. Run the `/models` command to select a model like _Mistral Medium_. - ```txt - /models - ``` + ```txt + /models + ``` --- From 3fe602cda3e34761256c53254accc46abdff1c17 Mon Sep 17 00:00:00 2001 From: Ismail Ghallou Date: Fri, 17 Apr 2026 17:29:31 +0200 Subject: [PATCH 095/335] feat: add LLM Gateway provider (#7847) Co-authored-by: Claude Opus 4.5 Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Aiden Cline --- packages/opencode/src/provider/provider.ts | 11 +++ packages/opencode/src/provider/transform.ts | 4 +- packages/opencode/test/preload.ts | 1 + .../ui/src/components/provider-icons/types.ts | 1 + packages/web/src/content/docs/providers.mdx | 68 +++++++++++++++++++ 5 files changed, 83 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 711481d80a..558bcb75a5 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -390,6 +390,17 @@ function custom(dep: CustomDep): Record { }, } }), + llmgateway: () => + Effect.succeed({ + autoload: false, + options: { + headers: { + "HTTP-Referer": "https://opencode.ai/", + "X-Title": "opencode", + "X-Source": "opencode", + }, + }, + }), openrouter: () => Effect.succeed({ autoload: false, diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 284fb0fcad..1b6b0918b1 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -807,7 +807,7 @@ export function options(input: { result["promptCacheKey"] = input.sessionID } - if (input.model.api.npm === "@openrouter/ai-sdk-provider") { + if (input.model.api.npm === "@openrouter/ai-sdk-provider" || input.model.api.npm === "@llmgateway/ai-sdk-provider") { result["usage"] = { include: true, } @@ -944,7 +944,7 @@ export function smallOptions(model: Provider.Model) { } return { thinkingConfig: { thinkingBudget: 0 } } } - if (model.providerID === "openrouter") { + if (model.providerID === "openrouter" || model.providerID === "llmgateway") { if (model.api.id.includes("google")) { return { reasoning: { enabled: false } } } diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index a2592286ad..58dc2b0b48 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -62,6 +62,7 @@ delete process.env["AWS_PROFILE"] delete process.env["AWS_REGION"] delete process.env["AWS_BEARER_TOKEN_BEDROCK"] delete process.env["OPENROUTER_API_KEY"] +delete process.env["LLM_GATEWAY_API_KEY"] delete process.env["GROQ_API_KEY"] delete process.env["MISTRAL_API_KEY"] delete process.env["PERPLEXITY_API_KEY"] diff --git a/packages/ui/src/components/provider-icons/types.ts b/packages/ui/src/components/provider-icons/types.ts index f9ddfdf0e9..5a97287509 100644 --- a/packages/ui/src/components/provider-icons/types.ts +++ b/packages/ui/src/components/provider-icons/types.ts @@ -32,6 +32,7 @@ export const iconNames = [ "perplexity", "ovhcloud", "openrouter", + "llmgateway", "opencode", "opencode-go", "openai", diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index 163dcdcf2b..bad9e1ebbc 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -1577,6 +1577,74 @@ OpenCode Zen is a list of tested and verified models provided by the OpenCode te --- +### LLM Gateway + +1. Head over to the [LLM Gateway dashboard](https://llmgateway.io/dashboard), click **Create API Key**, and copy the key. + +2. Run the `/connect` command and search for LLM Gateway. + + ```txt + /connect + ``` + +3. Enter the API key for the provider. + + ```txt + ┌ API key + │ + │ + └ enter + ``` + +4. Many LLM Gateway models are preloaded by default, run the `/models` command to select the one you want. + + ```txt + /models + ``` + + You can also add additional models through your opencode config. + + ```json title="opencode.json" {6} + { + "$schema": "https://opencode.ai/config.json", + "provider": { + "llmgateway": { + "models": { + "somecoolnewmodel": {} + } + } + } + } + ``` + +5. You can also customize them through your opencode config. Here's an example of specifying a provider + + ```json title="opencode.json" + { + "$schema": "https://opencode.ai/config.json", + "provider": { + "llmgateway": { + "models": { + "glm-4.7": { + "name": "GLM 4.7" + }, + "gpt-5.2": { + "name": "GPT-5.2" + }, + "gemini-2.5-pro": { + "name": "Gemini 2.5 Pro" + }, + "claude-3-5-sonnet-20241022": { + "name": "Claude 3.5 Sonnet" + } + } + } + } + } + ``` + +--- + ### SAP AI Core SAP AI Core provides access to 40+ models from OpenAI, Anthropic, Google, Amazon, Meta, Mistral, and AI21 through a unified platform. From 38cd3979f20b4d7842268b501568bf930b8e867a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 15:31:50 +0000 Subject: [PATCH 096/335] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index c19a837e84..54fd991eca 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-yGb+EPlFNDptIi4yFdJ0z7fhAyfOCRXu0GpNxrOnLVA=", - "aarch64-linux": "sha256-h8oBwLB6LnFN4xGB0/ocvxRbBMwewrEck91ADihTUCk=", - "aarch64-darwin": "sha256-/0IOgoihi4OR2AhDDfstLn2DcY/261v/KRQV4Pwhbmk=", - "x86_64-darwin": "sha256-rPkqF+led93s1plBbhFpszrnzF2H+EUz8QlfbUkSHvM=" + "x86_64-linux": "sha256-OPbZUo/fQv2Xsf+NEZV08GLBMN/DXovhRvn2JkesFtY=", + "aarch64-linux": "sha256-WK7xlVLuirKDN5LaqjBn7qpv5bYVtYHZw0qRNKX4xXg=", + "aarch64-darwin": "sha256-BAoAdeLQ+lXDD7Klxoxij683OVVug8KXEMRUqIQAjc8=", + "x86_64-darwin": "sha256-ZOBwNR2gZgc5f+y3VIBBT4qZpeZfg7Of6AaGDOfqsG8=" } } From 551216a452836a033e35bf88f64845bc966eef8f Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 17 Apr 2026 11:31:49 -0400 Subject: [PATCH 097/335] fix incorrect light mode in ghostty --- packages/opencode/src/cli/cmd/tui/context/theme.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 679be8f254..04670429da 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -397,7 +397,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ if (store.lock) return apply(mode) } - renderer.on(CliRenderEvents.THEME_MODE, handle) + // renderer.on(CliRenderEvents.THEME_MODE, handle) const refresh = () => { renderer.clearPaletteCache() From a27d3c162335946d3e6ed8a6a5a621006a88511e Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 17 Apr 2026 11:41:24 -0400 Subject: [PATCH 098/335] tui: fix session resumption with --session-id flag to navigate after app initialization Previously when passing a session ID directly, the route was set during initial render which could cause navigation issues before the router was fully ready. Now the session navigation happens after initialization completes, ensuring the TUI properly loads the requested session when users resume with --session-id. --- packages/opencode/src/cli/cmd/tui/app.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 74eca9a0f2..585e1ed232 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -150,7 +150,7 @@ export function tui(input: { Promise }) { }) local.model.set({ providerID, modelID }, { recent: true }) } + if (args.sessionID && !args.fork) { + route.navigate({ + type: "session", + sessionID: args.sessionID, + }) + } }) }) From 803d9eb7ad5f4dfd832d7506a7cad83ded52253e Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 17 Apr 2026 16:19:46 +0000 Subject: [PATCH 099/335] release: v1.4.9 --- bun.lock | 32 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop-electron/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++----- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/shared/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 19 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 776c5fb81e..f86e22af26 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.4.8", + "version": "1.4.9", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -83,7 +83,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.4.8", + "version": "1.4.9", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -117,7 +117,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.4.8", + "version": "1.4.9", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -144,7 +144,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.4.8", + "version": "1.4.9", "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/openai": "3.0.48", @@ -168,7 +168,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.4.8", + "version": "1.4.9", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -192,7 +192,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.4.8", + "version": "1.4.9", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -225,7 +225,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "1.4.8", + "version": "1.4.9", "dependencies": { "effect": "catalog:", "electron-context-menu": "4.1.2", @@ -268,7 +268,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.4.8", + "version": "1.4.9", "dependencies": { "@opencode-ai/shared": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -297,7 +297,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.4.8", + "version": "1.4.9", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -313,7 +313,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.4.8", + "version": "1.4.9", "bin": { "opencode": "./bin/opencode", }, @@ -458,7 +458,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.4.8", + "version": "1.4.9", "dependencies": { "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", @@ -493,7 +493,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.4.8", + "version": "1.4.9", "dependencies": { "cross-spawn": "catalog:", }, @@ -508,7 +508,7 @@ }, "packages/shared": { "name": "@opencode-ai/shared", - "version": "1.4.8", + "version": "1.4.9", "bin": { "opencode": "./bin/opencode", }, @@ -532,7 +532,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.4.8", + "version": "1.4.9", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -567,7 +567,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.4.8", + "version": "1.4.9", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -616,7 +616,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.4.8", + "version": "1.4.9", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 56ca71f892..0f4ae4228b 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.4.8", + "version": "1.4.9", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 528427ae80..63b6a5c414 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.4.8", + "version": "1.4.9", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 1b4292e9ad..e5351c1a87 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.4.8", + "version": "1.4.9", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index f3de599f1c..ef84eae470 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.4.8", + "version": "1.4.9", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index b13ad8520e..439beb15c8 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.4.8", + "version": "1.4.9", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index ed36f62827..b698a08896 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "1.4.8", + "version": "1.4.9", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index ce0ad5d4ab..1d9d24bd32 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.4.8", + "version": "1.4.9", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index f667de3822..02f1ad83ca 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.4.8", + "version": "1.4.9", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 31facbe3b5..ffcc975d1b 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.4.8" +version = "1.4.9" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.8/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.9/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.8/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.9/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.8/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.9/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.8/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.9/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.8/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.9/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 877c9dcd14..2ac49a9228 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.4.8", + "version": "1.4.9", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 7d9bfaccdd..3ce3b4192a 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.4.8", + "version": "1.4.9", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index d4958fa453..a1aa6470dc 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.4.8", + "version": "1.4.9", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 06213308f1..27d9188151 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.4.8", + "version": "1.4.9", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/shared/package.json b/packages/shared/package.json index 794c537da8..383b26d6ed 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.4.8", + "version": "1.4.9", "name": "@opencode-ai/shared", "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index d9865aa35c..75974ed39e 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.4.8", + "version": "1.4.9", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index f05ce03826..525cf20935 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.4.8", + "version": "1.4.9", "type": "module", "license": "MIT", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index a1d138319f..d3a2a25e4c 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.4.8", + "version": "1.4.9", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 07a3682c67..a3ed76c883 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.4.8", + "version": "1.4.9", "publisher": "sst-dev", "repository": { "type": "git", From 1a5913316850b256e497b899e29f7995302dcfa0 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 17 Apr 2026 12:00:22 -0400 Subject: [PATCH 100/335] Improve light mode dark mode copy --- packages/opencode/src/cli/cmd/tui/app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 585e1ed232..a58ff05648 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -606,7 +606,7 @@ function App(props: { onSnapshot?: () => Promise }) { category: "System", }, { - title: "Toggle theme mode", + title: mode() === "dark" ? "Switch to light mode" : "Switch to dark mode", value: "theme.switch_mode", onSelect: (dialog) => { setMode(mode() === "dark" ? "light" : "dark") From 0d582f9d3f9b20d5443fdee72aaf653778bf885e Mon Sep 17 00:00:00 2001 From: Vladimir Glafirov Date: Fri, 17 Apr 2026 18:22:43 +0200 Subject: [PATCH 101/335] chore: bump gitlab-ai-provider to 6.6.0 (#23057) --- bun.lock | 4 ++-- packages/opencode/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index f86e22af26..2442fcd709 100644 --- a/bun.lock +++ b/bun.lock @@ -386,7 +386,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.4.2", + "gitlab-ai-provider": "6.6.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -3314,7 +3314,7 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "gitlab-ai-provider": ["gitlab-ai-provider@6.4.2", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-Wyw6uslCuipBOr/NYwAtpgXEUJj68iJY5aekad2DjePN99JetKVQBqkLgAy9PZp2EA4OuscfRQu9qKIBN/evNw=="], + "gitlab-ai-provider": ["gitlab-ai-provider@6.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-jUxYnKA4XQaPc3wxACDZ8bPDXO0Mzx7cZaBDxbT2uGgLqtGZmSi+9tVNIg7louSS+s/ioVra3SoUz3iOFVhKPA=="], "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 3ce3b4192a..ace30e9bcb 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -143,7 +143,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.4.2", + "gitlab-ai-provider": "6.6.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", From fde3d9133bdf53d5bab9cf44f7f9ec4a23ae4fb4 Mon Sep 17 00:00:00 2001 From: rasdani <73563550+rasdani@users.noreply.github.com> Date: Fri, 17 Apr 2026 09:28:23 -0700 Subject: [PATCH 102/335] fix(opencode): pass `EXA_API_KEY` to `websearch` tool to avoid rate limits (#16362) Co-authored-by: Dax Raad Co-authored-by: Aiden Cline Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- packages/opencode/src/tool/mcp-exa.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/mcp-exa.ts b/packages/opencode/src/tool/mcp-exa.ts index 638d68c245..3340d84efd 100644 --- a/packages/opencode/src/tool/mcp-exa.ts +++ b/packages/opencode/src/tool/mcp-exa.ts @@ -1,7 +1,9 @@ import { Duration, Effect, Schema } from "effect" import { HttpClient, HttpClientRequest } from "effect/unstable/http" -const URL = "https://mcp.exa.ai/mcp" +const URL = process.env.EXA_API_KEY + ? `https://mcp.exa.ai/mcp?exaApiKey=${encodeURIComponent(process.env.EXA_API_KEY)}` + : "https://mcp.exa.ai/mcp" const McpResult = Schema.Struct({ result: Schema.Struct({ From c491161c0c6b69fe95672bba395a13b506940512 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:40:24 -0500 Subject: [PATCH 103/335] chore: bump @ai-sdk/anthropic to 3.0.71 and dependents (#23120) --- bun.lock | 16 ++++++++-------- packages/opencode/package.json | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bun.lock b/bun.lock index 2442fcd709..c78c9ae48d 100644 --- a/bun.lock +++ b/bun.lock @@ -322,15 +322,15 @@ "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.16.1", "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/amazon-bedrock": "4.0.94", - "@ai-sdk/anthropic": "3.0.70", + "@ai-sdk/amazon-bedrock": "4.0.95", + "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/azure": "3.0.49", "@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/gateway": "3.0.102", "@ai-sdk/google": "3.0.63", - "@ai-sdk/google-vertex": "4.0.111", + "@ai-sdk/google-vertex": "4.0.112", "@ai-sdk/groq": "3.0.31", "@ai-sdk/mistral": "3.0.27", "@ai-sdk/openai": "3.0.53", @@ -740,7 +740,7 @@ "@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="], - "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.94", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.70", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XKE7wAjXejsIfNQvn3onvGUByhGHVM6W+xlL+1DAQLmjEb+ue4sOJIRehJ96rEvTXVVHRVyA6bSXx7ayxXfn5A=="], + "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.95", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-qJKWEy+cNx3bLSJi/XpIVhv0P8KO0JFB1SvEroNWN8gKm820SIglBmXS10DTeXJdM5PPbQX4i/wJj5BHEk2LRQ=="], "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="], @@ -764,7 +764,7 @@ "@ai-sdk/google": ["@ai-sdk/google@3.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ=="], - "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.111", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.70", "@ai-sdk/google": "3.0.64", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5gILpAWWI5idfal/MfoH3tlQeSnOJ9jfL8JB8m2fdc3ue/9xoXkYDpXpDL/nyJImFjMCi6eR0Fpvlo/IKEWDIg=="], + "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.112", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/google": "3.0.64", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cSfHCkM+9ZrFtQWIN1WlV93JPD+isGSdFxKj7u1L9m2aLVZajlXdcE41GL9hMt7ld7bZYE4NnZ+4VLxBAHE+Eg=="], "@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="], @@ -5154,7 +5154,7 @@ "@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.70", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hubTFcfnG3NbrlcDW0tU2fsZhRy/7dF5GCymu4DzBQUYliy2lb7tCeeMhDtFBaYa01qSBHRjkwGnsAdUtDPCwA=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="], "@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.13", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ=="], @@ -5172,7 +5172,7 @@ "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], - "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.70", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hubTFcfnG3NbrlcDW0tU2fsZhRy/7dF5GCymu4DzBQUYliy2lb7tCeeMhDtFBaYa01qSBHRjkwGnsAdUtDPCwA=="], + "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="], "@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA=="], @@ -5922,7 +5922,7 @@ "nypm/tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], - "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.70", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hubTFcfnG3NbrlcDW0tU2fsZhRy/7dF5GCymu4DzBQUYliy2lb7tCeeMhDtFBaYa01qSBHRjkwGnsAdUtDPCwA=="], + "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="], "opencode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index ace30e9bcb..cadb1fa38d 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -79,15 +79,15 @@ "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.16.1", "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/amazon-bedrock": "4.0.94", - "@ai-sdk/anthropic": "3.0.70", + "@ai-sdk/amazon-bedrock": "4.0.95", + "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/azure": "3.0.49", "@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/gateway": "3.0.102", "@ai-sdk/google": "3.0.63", - "@ai-sdk/google-vertex": "4.0.111", + "@ai-sdk/google-vertex": "4.0.112", "@ai-sdk/groq": "3.0.31", "@ai-sdk/mistral": "3.0.27", "@ai-sdk/openai": "3.0.53", From 13dfe569efda341cb7b6d4d163e4aee471e65043 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 12:45:29 -0400 Subject: [PATCH 104/335] tui: fix agent cycling and prompt metadata polish (#23115) --- .../cli/cmd/tui/component/dialog-command.tsx | 1 + .../cli/cmd/tui/component/prompt/index.tsx | 24 ++++++++++++------- .../src/cli/cmd/tui/context/local.tsx | 4 +++- .../opencode/src/cli/cmd/tui/util/signal.ts | 7 ++++-- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx index f42ba15ec0..49bf42c63e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx @@ -63,6 +63,7 @@ function init() { useKeyboard((evt) => { if (suspended()) return if (dialog.stack.length > 0) return + if (evt.defaultPrevented) return for (const option of entries()) { if (!isEnabled(option)) continue if (option.keybind && keybind.match(option.keybind, evt)) { diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 06e5a0884e..98527bf912 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -5,7 +5,7 @@ import path from "path" import { fileURLToPath } from "url" import { Filesystem } from "@/util" import { useLocal } from "@tui/context/local" -import { useTheme } from "@tui/context/theme" +import { tint, useTheme } from "@tui/context/theme" import { EmptyBorder, SplitBorder } from "@tui/component/border" import { useSDK } from "@tui/context/sdk" import { useRoute } from "@tui/context/route" @@ -463,19 +463,25 @@ export function Prompt(props: PromptProps) { createEffect(() => { if (!input || input.isDestroyed) return if (props.visible === false || dialog.stack.length > 0) { - input.blur() + if (input.focused) input.blur() return } // Slot/plugin updates can remount the background prompt while a dialog is open. // Keep focus with the dialog and let the prompt reclaim it after the dialog closes. - input.focus() + if (!input.focused) input.focus() }) createEffect(() => { if (!input || input.isDestroyed) return + const capture = + store.mode === "normal" + ? auto()?.visible + ? (["escape", "navigate", "submit", "tab"] as const) + : (["tab"] as const) + : undefined input.traits = { - capture: auto()?.visible ? ["escape", "navigate", "submit", "tab"] : undefined, + capture, suspend: !!props.disabled || store.mode === "shell", status: store.mode === "shell" ? "SHELL" : undefined, } @@ -870,6 +876,7 @@ export function Prompt(props: PromptProps) { () => !!local.agent.current() && store.mode === "normal" && showVariant(), animationsEnabled, ) + const borderHighlight = createMemo(() => tint(theme.border, highlight(), agentMetaAlpha())) const placeholderText = createMemo(() => { if (props.showPlaceholder === false) return undefined @@ -931,7 +938,7 @@ export function Prompt(props: PromptProps) { (anchor = r)} visible={props.visible !== false}> }> {(agent) => ( <> - - {store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)}{" "} - + {store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)} + · { - let next = agents().findIndex((x) => x.name === agentStore.current) + direction + const current = this.current() + if (!current) return + let next = agents().findIndex((x) => x.name === current.name) + direction if (next < 0) next = agents().length - 1 if (next >= agents().length) next = 0 const value = agents()[next] diff --git a/packages/opencode/src/cli/cmd/tui/util/signal.ts b/packages/opencode/src/cli/cmd/tui/util/signal.ts index 1c7cc0008d..7d20ae04ba 100644 --- a/packages/opencode/src/cli/cmd/tui/util/signal.ts +++ b/packages/opencode/src/cli/cmd/tui/util/signal.ts @@ -8,20 +8,23 @@ export function createDebouncedSignal(value: T, ms: number): [Accessor, Sc export function createFadeIn(show: Accessor, enabled: Accessor) { const [alpha, setAlpha] = createSignal(show() ? 1 : 0) + let revealed = show() createEffect( - on([show, enabled], ([visible, animate], previous) => { + on([show, enabled], ([visible, animate]) => { if (!visible) { setAlpha(0) return } - if (!animate || !previous) { + if (!animate || revealed) { + revealed = true setAlpha(1) return } const start = performance.now() + revealed = true setAlpha(0) const timer = setInterval(() => { From ce0cfb0ea5c86af77b97fc41e9076721f601c052 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 16:46:34 +0000 Subject: [PATCH 105/335] chore: generate --- packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 98527bf912..cf26ec1950 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1153,7 +1153,9 @@ export function Prompt(props: PromptProps) { }> {(agent) => ( <> - {store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)} + + {store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)} + · From 797953c88d29319deb8e003594f31fe6d2e9b8b3 Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 17 Apr 2026 13:01:22 -0400 Subject: [PATCH 106/335] when generating sdk only format sdk, much faster (#23122) --- script/format.ts | 4 +++- script/generate.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/script/format.ts b/script/format.ts index 996de9ad04..37ab2197d0 100755 --- a/script/format.ts +++ b/script/format.ts @@ -2,4 +2,6 @@ import { $ } from "bun" -await $`bun run prettier --ignore-unknown --write .` +const dir = Bun.argv[2] ?? "." + +await $`bun run prettier --ignore-unknown --write ${dir}` diff --git a/script/generate.ts b/script/generate.ts index 8fc251d89d..a99d3e77ef 100755 --- a/script/generate.ts +++ b/script/generate.ts @@ -6,4 +6,4 @@ await $`bun ./packages/sdk/js/script/build.ts` await $`bun dev generate > ../sdk/openapi.json`.cwd("packages/opencode") -await $`./script/format.ts` +await $`./script/format.ts ./packages/sdk` From fcb473ff64f0767461c27db8942ce41df3e115d3 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 17:25:44 +0000 Subject: [PATCH 107/335] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 54fd991eca..5fe1189579 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-OPbZUo/fQv2Xsf+NEZV08GLBMN/DXovhRvn2JkesFtY=", - "aarch64-linux": "sha256-WK7xlVLuirKDN5LaqjBn7qpv5bYVtYHZw0qRNKX4xXg=", - "aarch64-darwin": "sha256-BAoAdeLQ+lXDD7Klxoxij683OVVug8KXEMRUqIQAjc8=", - "x86_64-darwin": "sha256-ZOBwNR2gZgc5f+y3VIBBT4qZpeZfg7Of6AaGDOfqsG8=" + "x86_64-linux": "sha256-60KBy/mySuIKbBD5aCS4ZAQqnwZ4PjLMAqZH7gpKCFY=", + "aarch64-linux": "sha256-ROd9OfdCBQZntoAr32O3YVl7ljRgYvJma25U+jHwcts=", + "aarch64-darwin": "sha256-MjMfR73ZZLXtIXfuzqpjvD5RxmIRi9HA1jWXPvagU6w=", + "x86_64-darwin": "sha256-1BADHsSdMxJUbQ4DR/Ww4ZTt18H365lETJs7Fy7fsLc=" } } From a8c78fc005a9a1cff3622a68f65b1df550cb2ccc Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 17 Apr 2026 13:30:09 -0400 Subject: [PATCH 108/335] fix(core): add historical sync on workspace connect (#23121) --- .../opencode/src/cli/cmd/tui/context/sdk.tsx | 28 +++++- .../opencode/src/control-plane/workspace.ts | 97 ++++++++++++++++--- packages/opencode/src/server/proxy.ts | 7 -- .../src/server/routes/instance/sync.ts | 26 ++++- .../test/workspace/workspace-restore.test.ts | 5 +- packages/sdk/js/src/v2/gen/sdk.gen.ts | 33 ++++++- packages/sdk/js/src/v2/gen/types.gen.ts | 19 ++++ packages/sdk/openapi.json | 43 +++++++- 8 files changed, 234 insertions(+), 24 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx index 14d3062886..6a240ceef8 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx @@ -2,6 +2,7 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2" import type { GlobalEvent } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "./helper" import { createGlobalEmitter } from "@solid-primitives/event-bus" +import { Flag } from "@/flag/flag" import { batch, onCleanup, onMount } from "solid-js" export type EventSource = { @@ -39,6 +40,8 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ let queue: GlobalEvent[] = [] let timer: Timer | undefined let last = 0 + const retryDelay = 1000 + const maxRetryDelay = 30000 const flush = () => { if (queue.length === 0) return @@ -73,9 +76,20 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ const ctrl = new AbortController() sse = ctrl ;(async () => { + let attempt = 0 while (true) { if (abort.signal.aborted || ctrl.signal.aborted) break - const events = await sdk.global.event({ signal: ctrl.signal }) + + const events = await sdk.global.event({ + signal: ctrl.signal, + sseMaxRetryAttempts: 0, + }) + + if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) { + // Start syncing workspaces, it's important to do this after + // we've started listening to events + await sdk.sync.start().catch(() => {}) + } for await (const event of events.stream) { if (ctrl.signal.aborted) break @@ -84,6 +98,12 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ if (timer) clearTimeout(timer) if (queue.length > 0) flush() + attempt += 1 + if (abort.signal.aborted || ctrl.signal.aborted) break + + // Exponential backoff + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), maxRetryDelay) + await new Promise((resolve) => setTimeout(resolve, backoff)) } })().catch(() => {}) } @@ -92,6 +112,12 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ if (props.events) { const unsub = await props.events.subscribe(handleEvent) onCleanup(unsub) + + if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) { + // Start syncing workspaces, it's important to do this after + // we've started listening to events + await sdk.sync.start().catch(() => {}) + } } else { startSSE() } diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index fd22d3af04..d678ad7526 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -7,7 +7,7 @@ import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" import { Auth } from "@/auth" import { SyncEvent } from "@/sync" -import { EventTable } from "@/sync/event.sql" +import { EventSequenceTable, EventTable } from "@/sync/event.sql" import { Flag } from "@/flag/flag" import { Log } from "@/util" import { Filesystem } from "@/util" @@ -23,8 +23,8 @@ import { SessionTable } from "@/session/session.sql" import { SessionID } from "@/session/schema" import { errorData } from "@/util/error" import { AppRuntime } from "@/effect/app-runtime" -import { EventSequenceTable } from "@/sync/event.sql" import { waitEvent } from "./util" +import { WorkspaceContext } from "./workspace-context" export const Info = WorkspaceInfo.meta({ ref: "Workspace", @@ -297,22 +297,13 @@ export function list(project: Project.Info) { db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(), ) const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id)) - - for (const space of spaces) startSync(space) return spaces } -function lookup(id: WorkspaceID) { +export const get = fn(WorkspaceID.zod, async (id) => { const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get()) if (!row) return return fromRow(row) -} - -export const get = fn(WorkspaceID.zod, async (id) => { - const space = lookup(id) - if (!space) return - startSync(space) - return space }) export const remove = fn(WorkspaceID.zod, async (id) => { @@ -437,6 +428,70 @@ async function connectSSE(url: URL | string, headers: HeadersInit | undefined, s return res.body } +async function syncHistory(space: Info, url: URL | string, headers: HeadersInit | undefined, signal: AbortSignal) { + const sessionIDs = Database.use((db) => + db + .select({ id: SessionTable.id }) + .from(SessionTable) + .where(eq(SessionTable.workspace_id, space.id)) + .all() + .map((row) => row.id), + ) + const state = sessionIDs.length + ? Object.fromEntries( + Database.use((db) => + db.select().from(EventSequenceTable).where(inArray(EventSequenceTable.aggregate_id, sessionIDs)).all(), + ).map((row) => [row.aggregate_id, row.seq]), + ) + : {} + + log.info("syncing workspace history", { + workspaceID: space.id, + sessions: sessionIDs.length, + known: Object.keys(state).length, + }) + + const requestHeaders = new Headers(headers) + requestHeaders.set("content-type", "application/json") + + const res = await fetch(route(url, "/sync/history"), { + method: "POST", + headers: requestHeaders, + body: JSON.stringify(state), + signal, + }) + + if (!res.ok) { + const body = await res.text() + throw new Error(`Workspace history HTTP failure: ${res.status} ${body}`) + } + + const events = await res.json() + + return WorkspaceContext.provide({ + workspaceID: space.id, + fn: () => { + for (const event of events) { + SyncEvent.replay( + { + id: event.id, + aggregateID: event.aggregate_id, + seq: event.seq, + type: event.type, + data: event.data, + }, + { publish: true }, + ) + } + }, + }) + + log.info("workspace history synced", { + workspaceID: space.id, + events: events.length, + }) +} + async function syncWorkspaceLoop(space: Info, signal: AbortSignal) { const adaptor = await getAdaptor(space.projectID, space.type) const target = await adaptor.target(space) @@ -452,7 +507,9 @@ async function syncWorkspaceLoop(space: Info, signal: AbortSignal) { let stream try { stream = await connectSSE(target.url, target.headers, signal) + await syncHistory(space, target.url, target.headers, signal) } catch (err) { + stream = null setStatus(space.id, "error") log.info("failed to connect to global sync", { workspace: space.name, @@ -469,6 +526,7 @@ async function syncWorkspaceLoop(space: Info, signal: AbortSignal) { await parseSSE(stream, signal, (evt: any) => { try { if (!("payload" in evt)) return + if (evt.payload.type === "server.heartbeat") return if (evt.payload.type === "sync") { SyncEvent.replay(evt.payload.syncEvent as SyncEvent.SerializedEvent) @@ -536,4 +594,19 @@ function stopSync(id: WorkspaceID) { connections.delete(id) } +export function startWorkspaceSyncing(projectID: ProjectID) { + const spaces = Database.use((db) => + db + .select({ workspace: WorkspaceTable }) + .from(WorkspaceTable) + .innerJoin(SessionTable, eq(SessionTable.workspace_id, WorkspaceTable.id)) + .where(eq(WorkspaceTable.project_id, projectID)) + .all(), + ) + + for (const row of new Map(spaces.map((row) => [row.workspace.id, row.workspace])).values()) { + void startSync(fromRow(row)) + } +} + export * as Workspace from "./workspace" diff --git a/packages/opencode/src/server/proxy.ts b/packages/opencode/src/server/proxy.ts index 9c1fd1f288..19a623cb0c 100644 --- a/packages/opencode/src/server/proxy.ts +++ b/packages/opencode/src/server/proxy.ts @@ -130,13 +130,6 @@ export async function http(url: string | URL, extra: HeadersInit | undefined, re const done = sync ? Fence.wait(workspaceID, sync, req.signal) : Promise.resolve() return done.then(async () => { - console.log("proxy http response", { - method: req.method, - request: req.url, - url: String(url), - status: res.status, - statusText: res.statusText, - }) return new Response(res.body, { status: res.status, statusText: res.statusText, diff --git a/packages/opencode/src/server/routes/instance/sync.ts b/packages/opencode/src/server/routes/instance/sync.ts index c6a067997b..b124cd875d 100644 --- a/packages/opencode/src/server/routes/instance/sync.ts +++ b/packages/opencode/src/server/routes/instance/sync.ts @@ -6,6 +6,8 @@ import { Database, asc, and, not, or, lte, eq } from "@/storage" import { EventTable } from "@/sync/event.sql" import { lazy } from "@/util/lazy" import { Log } from "@/util" +import { startWorkspaceSyncing } from "@/control-plane/workspace" +import { Instance } from "@/project/instance" import { errors } from "../../error" const ReplayEvent = z.object({ @@ -20,6 +22,28 @@ const log = Log.create({ service: "server.sync" }) export const SyncRoutes = lazy(() => new Hono() + .post( + "/start", + describeRoute({ + summary: "Start workspace sync", + description: "Start sync loops for workspaces in the current project that have active sessions.", + operationId: "sync.start", + responses: { + 200: { + description: "Workspace sync started", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + }, + }), + async (c) => { + startWorkspaceSyncing(Instance.project.id) + return c.json(true) + }, + ) .post( "/replay", describeRoute({ @@ -75,7 +99,7 @@ export const SyncRoutes = lazy(() => }) }, ) - .get( + .post( "/history", describeRoute({ summary: "List sync events", diff --git a/packages/opencode/test/workspace/workspace-restore.test.ts b/packages/opencode/test/workspace/workspace-restore.test.ts index 429eeaf9dd..ad6ac2c5fd 100644 --- a/packages/opencode/test/workspace/workspace-restore.test.ts +++ b/packages/opencode/test/workspace/workspace-restore.test.ts @@ -141,9 +141,12 @@ describe("Workspace.sessionRestore", () => { Object.assign( async (input: URL | RequestInfo, init?: BunFetchRequestInit | RequestInit) => { const url = new URL(typeof input === "string" || input instanceof URL ? input : input.url) - if (url.pathname !== "/base/sync/replay") { + if (url.pathname === "/base/global/event") { return eventStreamResponse() } + if (url.pathname === "/base/sync/history") { + return Response.json([]) + } const body = JSON.parse(String(init?.body)) posts.push({ path: url.pathname, diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index f484147a40..6248eb8e4d 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -163,6 +163,7 @@ import type { SyncHistoryListResponses, SyncReplayErrors, SyncReplayResponses, + SyncStartResponses, TextPartInput, ToolIdsErrors, ToolIdsResponses, @@ -3038,7 +3039,7 @@ export class History extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ + return (options?.client ?? this.client).post({ url: "/sync/history", ...options, ...params, @@ -3052,6 +3053,36 @@ export class History extends HeyApiClient { } export class Sync extends HeyApiClient { + /** + * Start workspace sync + * + * Start sync loops for workspaces in the current project that have active sessions. + */ + public start( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/sync/start", + ...options, + ...params, + }) + } + /** * Replay sync events * diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 460c2bcdfa..5698cba54f 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4502,6 +4502,25 @@ export type ProviderOauthCallbackResponses = { export type ProviderOauthCallbackResponse = ProviderOauthCallbackResponses[keyof ProviderOauthCallbackResponses] +export type SyncStartData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/sync/start" +} + +export type SyncStartResponses = { + /** + * Workspace sync started + */ + 200: boolean +} + +export type SyncStartResponse = SyncStartResponses[keyof SyncStartResponses] + export type SyncReplayData = { body?: { directory: string diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 7bdf025bbe..3b811f2fa9 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -5224,6 +5224,47 @@ ] } }, + "/sync/start": { + "post": { + "operationId": "sync.start", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + } + ], + "summary": "Start workspace sync", + "description": "Start sync loops for workspaces in the current project that have active sessions.", + "responses": { + "200": { + "description": "Workspace sync started", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.sync.start({\n ...\n})" + } + ] + } + }, "/sync/replay": { "post": { "operationId": "sync.replay", @@ -5328,7 +5369,7 @@ } }, "/sync/history": { - "get": { + "post": { "operationId": "sync.history.list", "parameters": [ { From 4c30a78cd9623fe8f3a7c27860a7b8a0cc760e39 Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 17 Apr 2026 13:33:11 -0400 Subject: [PATCH 109/335] fix: revert sdk generation script change (#23133) --- script/format.ts | 4 +--- script/generate.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/script/format.ts b/script/format.ts index 37ab2197d0..996de9ad04 100755 --- a/script/format.ts +++ b/script/format.ts @@ -2,6 +2,4 @@ import { $ } from "bun" -const dir = Bun.argv[2] ?? "." - -await $`bun run prettier --ignore-unknown --write ${dir}` +await $`bun run prettier --ignore-unknown --write .` diff --git a/script/generate.ts b/script/generate.ts index a99d3e77ef..8fc251d89d 100755 --- a/script/generate.ts +++ b/script/generate.ts @@ -6,4 +6,4 @@ await $`bun ./packages/sdk/js/script/build.ts` await $`bun dev generate > ../sdk/openapi.json`.cwd("packages/opencode") -await $`./script/format.ts ./packages/sdk` +await $`./script/format.ts` From 2f73e73e9d03262fb59d4e942b3e1e073cb76cb9 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 17 Apr 2026 14:08:23 -0400 Subject: [PATCH 110/335] trace npm fully --- .opencode/opencode.jsonc | 6 +- .../opencode/src/cli/cmd/tui/config/tui.ts | 4 +- packages/opencode/src/cli/cmd/tui/layer.ts | 2 +- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/effect/app-runtime.ts | 5 +- .../opencode/src/effect/bootstrap-runtime.ts | 2 +- packages/opencode/src/effect/memo-map.ts | 3 + packages/opencode/src/effect/run-service.ts | 3 +- .../opencode/src/{cli => }/effect/runtime.ts | 5 +- packages/opencode/src/npm/effect.ts | 261 ++++++++++++++++++ packages/opencode/src/plugin/shared.ts | 2 +- .../server/routes/instance/httpapi/server.ts | 2 +- packages/opencode/test/config/config.test.ts | 2 +- packages/opencode/test/tool/read.test.ts | 1 - packages/shared/src/npm.ts | 249 ----------------- packages/shared/test/npm.test.ts | 18 -- 16 files changed, 279 insertions(+), 288 deletions(-) create mode 100644 packages/opencode/src/effect/memo-map.ts rename packages/opencode/src/{cli => }/effect/runtime.ts (90%) create mode 100644 packages/opencode/src/npm/effect.ts delete mode 100644 packages/shared/src/npm.ts delete mode 100644 packages/shared/test/npm.test.ts diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index 8380f7f719..82ab6d1b35 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -1,10 +1,6 @@ { "$schema": "https://opencode.ai/config.json", - "provider": { - "opencode": { - "options": {}, - }, - }, + "provider": {}, "permission": { "edit": { "packages/opencode/migration/*": "deny", diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index a5c9ae0430..abcf11fcef 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -11,14 +11,14 @@ import { Flag } from "@/flag/flag" import { isRecord } from "@/util/record" import { Global } from "@/global" import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Npm } from "@opencode-ai/shared/npm" import { CurrentWorkingDirectory } from "./cwd" import { ConfigPlugin } from "@/config/plugin" import { ConfigKeybinds } from "@/config/keybinds" import { InstallationLocal, InstallationVersion } from "@/installation/version" -import { makeRuntime } from "@/cli/effect/runtime" +import { makeRuntime } from "@/effect/runtime" import { Filesystem, Log } from "@/util" import { ConfigVariable } from "@/config/variable" +import { Npm } from "@/npm/effect" const log = Log.create({ service: "tui.config" }) diff --git a/packages/opencode/src/cli/cmd/tui/layer.ts b/packages/opencode/src/cli/cmd/tui/layer.ts index 734106f8a6..66497f8b1a 100644 --- a/packages/opencode/src/cli/cmd/tui/layer.ts +++ b/packages/opencode/src/cli/cmd/tui/layer.ts @@ -1,6 +1,6 @@ import { Layer } from "effect" import { TuiConfig } from "./config/tui" -import { Npm } from "@opencode-ai/shared/npm" +import { Npm } from "@/npm/effect" import { Observability } from "@/effect/observability" export const CliLayer = Observability.layer.pipe(Layer.merge(TuiConfig.layer), Layer.provide(Npm.defaultLayer)) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ebd4a41fcb..8980765b79 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -24,7 +24,6 @@ import { InstanceState } from "@/effect" import { Context, Duration, Effect, Exit, Fiber, Layer, Option } from "effect" import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" import { InstanceRef } from "@/effect/instance-ref" -import { Npm } from "@opencode-ai/shared/npm" import { ConfigAgent } from "./agent" import { ConfigMCP } from "./mcp" import { ConfigModelID } from "./model-id" @@ -39,6 +38,7 @@ import { ConfigPaths } from "./paths" import { ConfigFormatter } from "./formatter" import { ConfigLSP } from "./lsp" import { ConfigVariable } from "./variable" +import { Npm } from "@/npm/effect" const log = Log.create({ service: "config" }) diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index eae52d6366..262d85e7ea 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -1,5 +1,5 @@ import { Layer, ManagedRuntime } from "effect" -import { attach, memoMap } from "./run-service" +import { attach } from "./run-service" import * as Observability from "./observability" import { AppFileSystem } from "@opencode-ai/shared/filesystem" @@ -46,7 +46,8 @@ import { Pty } from "@/pty" import { Installation } from "@/installation" import { ShareNext } from "@/share" import { SessionShare } from "@/share" -import { Npm } from "@opencode-ai/shared/npm" +import { Npm } from "@/npm/effect" +import { memoMap } from "./memo-map" export const AppLayer = Layer.mergeAll( Npm.defaultLayer, diff --git a/packages/opencode/src/effect/bootstrap-runtime.ts b/packages/opencode/src/effect/bootstrap-runtime.ts index 62b71e58b1..37698c43a5 100644 --- a/packages/opencode/src/effect/bootstrap-runtime.ts +++ b/packages/opencode/src/effect/bootstrap-runtime.ts @@ -1,5 +1,4 @@ import { Layer, ManagedRuntime } from "effect" -import { memoMap } from "./run-service" import { Plugin } from "@/plugin" import { LSP } from "@/lsp" @@ -12,6 +11,7 @@ import { Snapshot } from "@/snapshot" import { Bus } from "@/bus" import { Config } from "@/config" import * as Observability from "./observability" +import { memoMap } from "./memo-map" export const BootstrapLayer = Layer.mergeAll( Config.defaultLayer, diff --git a/packages/opencode/src/effect/memo-map.ts b/packages/opencode/src/effect/memo-map.ts new file mode 100644 index 0000000000..c797dbf42e --- /dev/null +++ b/packages/opencode/src/effect/memo-map.ts @@ -0,0 +1,3 @@ +import { Layer } from "effect" + +export const memoMap = Layer.makeMemoMapUnsafe() diff --git a/packages/opencode/src/effect/run-service.ts b/packages/opencode/src/effect/run-service.ts index 28265f9b27..98ff83ea59 100644 --- a/packages/opencode/src/effect/run-service.ts +++ b/packages/opencode/src/effect/run-service.ts @@ -6,8 +6,7 @@ import { InstanceRef, WorkspaceRef } from "./instance-ref" import * as Observability from "./observability" import { WorkspaceContext } from "@/control-plane/workspace-context" import type { InstanceContext } from "@/project/instance" - -export const memoMap = Layer.makeMemoMapUnsafe() +import { memoMap } from "./memo-map" type Refs = { instance?: InstanceContext diff --git a/packages/opencode/src/cli/effect/runtime.ts b/packages/opencode/src/effect/runtime.ts similarity index 90% rename from packages/opencode/src/cli/effect/runtime.ts rename to packages/opencode/src/effect/runtime.ts index 57b9f8ede9..ad7872f0b5 100644 --- a/packages/opencode/src/cli/effect/runtime.ts +++ b/packages/opencode/src/effect/runtime.ts @@ -1,7 +1,6 @@ -import { Observability } from "@/effect/observability" +import { Observability } from "./observability" import { Layer, type Context, ManagedRuntime, type Effect } from "effect" - -export const memoMap = Layer.makeMemoMapUnsafe() +import { memoMap } from "./memo-map" export function makeRuntime(service: Context.Service, layer: Layer.Layer) { let rt: ManagedRuntime.ManagedRuntime | undefined diff --git a/packages/opencode/src/npm/effect.ts b/packages/opencode/src/npm/effect.ts new file mode 100644 index 0000000000..10b5ff179c --- /dev/null +++ b/packages/opencode/src/npm/effect.ts @@ -0,0 +1,261 @@ +export * as Npm from "./effect" + +import path from "path" +import semver from "semver" +import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect" +import { NodeFileSystem } from "@effect/platform-node" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { Global } from "@opencode-ai/shared/global" +import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" + +import { makeRuntime } from "../effect/runtime" + +export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", { + add: Schema.Array(Schema.String).pipe(Schema.optional), + dir: Schema.String, + cause: Schema.optional(Schema.Defect), +}) {} + +export interface EntryPoint { + readonly directory: string + readonly entrypoint: Option.Option +} + +export interface Interface { + readonly add: (pkg: string) => Effect.Effect + readonly install: ( + dir: string, + input?: { add: string[] }, + ) => Effect.Effect + readonly outdated: (pkg: string, cachedVersion: string) => Effect.Effect + readonly which: (pkg: string) => Effect.Effect> +} + +export class Service extends Context.Service()("@opencode/Npm") {} + +const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined + +export function sanitize(pkg: string) { + if (!illegal) return pkg + return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("") +} + +const resolveEntryPoint = (name: string, dir: string): EntryPoint => { + let entrypoint: Option.Option + try { + const resolved = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir) + entrypoint = Option.some(resolved) + } catch { + entrypoint = Option.none() + } + return { + directory: dir, + entrypoint, + } +} + +interface ArboristNode { + name: string + path: string +} + +interface ArboristTree { + edgesOut: Map +} + +const reify = (input: { dir: string; add?: string[] }) => + Effect.gen(function* () { + const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist")) + const arborist = new Arborist({ + path: input.dir, + binLinks: true, + progress: false, + savePrefix: "", + ignoreScripts: true, + }) + return yield* Effect.tryPromise({ + try: () => + arborist.reify({ + add: input?.add || [], + save: true, + saveType: "prod", + }), + catch: (cause) => + new InstallFailedError({ + cause, + add: input?.add, + dir: input.dir, + }), + }) as Effect.Effect + }).pipe( + Effect.withSpan("Npm.reify", { + attributes: input, + }), + ) + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const afs = yield* AppFileSystem.Service + const global = yield* Global.Service + const fs = yield* FileSystem.FileSystem + const flock = yield* EffectFlock.Service + const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg)) + + const outdated = Effect.fn("Npm.outdated")(function* (pkg: string, cachedVersion: string) { + const response = yield* Effect.tryPromise({ + try: () => fetch(`https://registry.npmjs.org/${pkg}`), + catch: () => undefined, + }).pipe(Effect.orElseSucceed(() => undefined)) + + if (!response || !response.ok) { + return false + } + + const data = yield* Effect.tryPromise({ + try: () => response.json() as Promise<{ "dist-tags"?: { latest?: string } }>, + catch: () => undefined, + }).pipe(Effect.orElseSucceed(() => undefined)) + + const latestVersion = data?.["dist-tags"]?.latest + if (!latestVersion) { + return false + } + + const range = /[\s^~*xX<>|=]/.test(cachedVersion) + if (range) return !semver.satisfies(latestVersion, cachedVersion) + + return semver.lt(cachedVersion, latestVersion) + }) + + const add = Effect.fn("Npm.add")(function* (pkg: string) { + const dir = directory(pkg) + yield* flock.acquire(`npm-install:${dir}`) + + const tree = yield* reify({ dir, add: [pkg] }) + const first = tree.edgesOut.values().next().value?.to + if (!first) return yield* new InstallFailedError({ add: [pkg], dir }) + return resolveEntryPoint(first.name, first.path) + }, Effect.scoped) + + const install = Effect.fn("Npm.install")(function* (dir: string, input?: { add: string[] }) { + const canWrite = yield* afs.access(dir, { writable: true }).pipe( + Effect.as(true), + Effect.orElseSucceed(() => false), + ) + if (!canWrite) return + + yield* flock.acquire(`npm-install:${dir}`) + + yield* Effect.gen(function* () { + const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules")) + if (!nodeModulesExists) { + yield* reify({ add: input?.add, dir }) + return + } + }).pipe(Effect.withSpan("Npm.checkNodeModules")) + + yield* Effect.gen(function* () { + const pkg = yield* afs.readJson(path.join(dir, "package.json")).pipe(Effect.orElseSucceed(() => ({}))) + const lock = yield* afs.readJson(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => ({}))) + + const pkgAny = pkg as any + const lockAny = lock as any + const declared = new Set([ + ...Object.keys(pkgAny?.dependencies || {}), + ...Object.keys(pkgAny?.devDependencies || {}), + ...Object.keys(pkgAny?.peerDependencies || {}), + ...Object.keys(pkgAny?.optionalDependencies || {}), + ...(input?.add || []), + ]) + + const root = lockAny?.packages?.[""] || {} + const locked = new Set([ + ...Object.keys(root?.dependencies || {}), + ...Object.keys(root?.devDependencies || {}), + ...Object.keys(root?.peerDependencies || {}), + ...Object.keys(root?.optionalDependencies || {}), + ]) + + for (const name of declared) { + if (!locked.has(name)) { + yield* reify({ dir, add: input?.add }) + return + } + } + }).pipe(Effect.withSpan("Npm.checkDirty")) + + return + }, Effect.scoped) + + const which = Effect.fn("Npm.which")(function* (pkg: string) { + const dir = directory(pkg) + const binDir = path.join(dir, "node_modules", ".bin") + + const pick = Effect.fnUntraced(function* () { + const files = yield* fs.readDirectory(binDir).pipe(Effect.catch(() => Effect.succeed([] as string[]))) + + if (files.length === 0) return Option.none() + if (files.length === 1) return Option.some(files[0]) + + const pkgJson = yield* afs.readJson(path.join(dir, "node_modules", pkg, "package.json")).pipe(Effect.option) + + if (Option.isSome(pkgJson)) { + const parsed = pkgJson.value as { bin?: string | Record } + if (parsed?.bin) { + const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg + const bin = parsed.bin + if (typeof bin === "string") return Option.some(unscoped) + const keys = Object.keys(bin) + if (keys.length === 1) return Option.some(keys[0]) + return bin[unscoped] ? Option.some(unscoped) : Option.some(keys[0]) + } + } + + return Option.some(files[0]) + }) + + return yield* Effect.gen(function* () { + const bin = yield* pick() + if (Option.isSome(bin)) { + return Option.some(path.join(binDir, bin.value)) + } + + yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {})) + + yield* add(pkg) + + const resolved = yield* pick() + if (Option.isNone(resolved)) return Option.none() + return Option.some(path.join(binDir, resolved.value)) + }).pipe( + Effect.scoped, + Effect.orElseSucceed(() => Option.none()), + ) + }) + + return Service.of({ + add, + install, + outdated, + which, + }) + }), +) + +export const defaultLayer = layer.pipe( + Layer.provide(EffectFlock.layer), + Layer.provide(AppFileSystem.layer), + Layer.provide(Global.layer), + Layer.provide(NodeFileSystem.layer), +) + +const { runPromise } = makeRuntime(Service, defaultLayer) + +export async function install(...args: Parameters) { + return runPromise((svc) => svc.install(...args)) +} + +export async function add(...args: Parameters) { + return runPromise((svc) => svc.add(...args)) +} diff --git a/packages/opencode/src/plugin/shared.ts b/packages/opencode/src/plugin/shared.ts index 11f36c41ae..f431204fc4 100644 --- a/packages/opencode/src/plugin/shared.ts +++ b/packages/opencode/src/plugin/shared.ts @@ -2,9 +2,9 @@ import path from "path" import { fileURLToPath, pathToFileURL } from "url" import npa from "npm-package-arg" import semver from "semver" -import { Npm } from "../npm" import { Filesystem } from "@/util" import { isRecord } from "@/util/record" +import { Npm } from "@/npm/effect" // Old npm package names for plugins that are now built-in export const DEPRECATED_PLUGIN_PACKAGES = ["opencode-openai-codex-auth", "opencode-copilot-auth"] diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index b4442d6400..d012e2c166 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -4,7 +4,6 @@ import { HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http" import { AppRuntime } from "@/effect/app-runtime" import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref" import { Observability } from "@/effect" -import { memoMap } from "@/effect/run-service" import { Flag } from "@/flag/flag" import { InstanceBootstrap } from "@/project/bootstrap" import { Instance } from "@/project/instance" @@ -15,6 +14,7 @@ import { PermissionApi, permissionHandlers } from "./permission" import { ProjectApi, projectHandlers } from "./project" import { ProviderApi, providerHandlers } from "./provider" import { QuestionApi, questionHandlers } from "./question" +import { memoMap } from "@/effect/memo-map" const Query = Schema.Struct({ directory: Schema.optional(Schema.String), diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index a321b558cf..7b01ee626a 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -27,7 +27,7 @@ import { Global } from "../../src/global" import { ProjectID } from "../../src/project/schema" import { Filesystem } from "../../src/util" import { ConfigPlugin } from "@/config/plugin" -import { Npm } from "@opencode-ai/shared/npm" +import { Npm } from "@/npm/effect" const emptyAccount = Layer.mock(Account.Service)({ active: () => Effect.succeed(Option.none()), diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index c3d7074bfb..7456990ad0 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -15,7 +15,6 @@ import { Tool } from "../../src/tool" import { Filesystem } from "../../src/util" import { provideInstance, tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" -import { Npm } from "@opencode-ai/shared/npm" const FIXTURES_DIR = path.join(import.meta.dir, "fixtures") diff --git a/packages/shared/src/npm.ts b/packages/shared/src/npm.ts deleted file mode 100644 index 865e827b31..0000000000 --- a/packages/shared/src/npm.ts +++ /dev/null @@ -1,249 +0,0 @@ -import path from "path" -import semver from "semver" -import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect" -import { NodeFileSystem } from "@effect/platform-node" -import { AppFileSystem } from "./filesystem" -import { Global } from "./global" -import { EffectFlock } from "./util/effect-flock" - -export namespace Npm { - export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", { - add: Schema.Array(Schema.String).pipe(Schema.optional), - dir: Schema.String, - cause: Schema.optional(Schema.Defect), - }) {} - - export interface EntryPoint { - readonly directory: string - readonly entrypoint: Option.Option - } - - export interface Interface { - readonly add: (pkg: string) => Effect.Effect - readonly install: ( - dir: string, - input?: { add: string[] }, - ) => Effect.Effect - readonly outdated: (pkg: string, cachedVersion: string) => Effect.Effect - readonly which: (pkg: string) => Effect.Effect> - } - - export class Service extends Context.Service()("@opencode/Npm") {} - - const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined - - export function sanitize(pkg: string) { - if (!illegal) return pkg - return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("") - } - - const resolveEntryPoint = (name: string, dir: string): EntryPoint => { - let entrypoint: Option.Option - try { - const resolved = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir) - entrypoint = Option.some(resolved) - } catch { - entrypoint = Option.none() - } - return { - directory: dir, - entrypoint, - } - } - - interface ArboristNode { - name: string - path: string - } - - interface ArboristTree { - edgesOut: Map - } - - const reify = (input: { dir: string; add?: string[] }) => - Effect.gen(function* () { - const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist")) - const arborist = new Arborist({ - path: input.dir, - binLinks: true, - progress: false, - savePrefix: "", - ignoreScripts: true, - }) - return yield* Effect.tryPromise({ - try: () => - arborist.reify({ - add: input?.add || [], - save: true, - saveType: "prod", - }), - catch: (cause) => - new InstallFailedError({ - cause, - add: input?.add, - dir: input.dir, - }), - }) as Effect.Effect - }).pipe( - Effect.withSpan("Npm.reify", { - attributes: input, - }), - ) - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const afs = yield* AppFileSystem.Service - const global = yield* Global.Service - const fs = yield* FileSystem.FileSystem - const flock = yield* EffectFlock.Service - const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg)) - - const outdated = Effect.fn("Npm.outdated")(function* (pkg: string, cachedVersion: string) { - const response = yield* Effect.tryPromise({ - try: () => fetch(`https://registry.npmjs.org/${pkg}`), - catch: () => undefined, - }).pipe(Effect.orElseSucceed(() => undefined)) - - if (!response || !response.ok) { - return false - } - - const data = yield* Effect.tryPromise({ - try: () => response.json() as Promise<{ "dist-tags"?: { latest?: string } }>, - catch: () => undefined, - }).pipe(Effect.orElseSucceed(() => undefined)) - - const latestVersion = data?.["dist-tags"]?.latest - if (!latestVersion) { - return false - } - - const range = /[\s^~*xX<>|=]/.test(cachedVersion) - if (range) return !semver.satisfies(latestVersion, cachedVersion) - - return semver.lt(cachedVersion, latestVersion) - }) - - const add = Effect.fn("Npm.add")(function* (pkg: string) { - const dir = directory(pkg) - yield* flock.acquire(`npm-install:${dir}`) - - const tree = yield* reify({ dir, add: [pkg] }) - const first = tree.edgesOut.values().next().value?.to - if (!first) return yield* new InstallFailedError({ add: [pkg], dir }) - return resolveEntryPoint(first.name, first.path) - }, Effect.scoped) - - const install = Effect.fn("Npm.install")(function* (dir: string, input?: { add: string[] }) { - const canWrite = yield* afs.access(dir, { writable: true }).pipe( - Effect.as(true), - Effect.orElseSucceed(() => false), - ) - if (!canWrite) return - - yield* flock.acquire(`npm-install:${dir}`) - - yield* Effect.gen(function* () { - const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules")) - if (!nodeModulesExists) { - yield* reify({ add: input?.add, dir }) - return - } - }).pipe(Effect.withSpan("Npm.checkNodeModules")) - - yield* Effect.gen(function* () { - const pkg = yield* afs.readJson(path.join(dir, "package.json")).pipe(Effect.orElseSucceed(() => ({}))) - const lock = yield* afs.readJson(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => ({}))) - - const pkgAny = pkg as any - const lockAny = lock as any - const declared = new Set([ - ...Object.keys(pkgAny?.dependencies || {}), - ...Object.keys(pkgAny?.devDependencies || {}), - ...Object.keys(pkgAny?.peerDependencies || {}), - ...Object.keys(pkgAny?.optionalDependencies || {}), - ...(input?.add || []), - ]) - - const root = lockAny?.packages?.[""] || {} - const locked = new Set([ - ...Object.keys(root?.dependencies || {}), - ...Object.keys(root?.devDependencies || {}), - ...Object.keys(root?.peerDependencies || {}), - ...Object.keys(root?.optionalDependencies || {}), - ]) - - for (const name of declared) { - if (!locked.has(name)) { - yield* reify({ dir, add: input?.add }) - return - } - } - }).pipe(Effect.withSpan("Npm.checkDirty")) - - return - }, Effect.scoped) - - const which = Effect.fn("Npm.which")(function* (pkg: string) { - const dir = directory(pkg) - const binDir = path.join(dir, "node_modules", ".bin") - - const pick = Effect.fnUntraced(function* () { - const files = yield* fs.readDirectory(binDir).pipe(Effect.catch(() => Effect.succeed([] as string[]))) - - if (files.length === 0) return Option.none() - if (files.length === 1) return Option.some(files[0]) - - const pkgJson = yield* afs.readJson(path.join(dir, "node_modules", pkg, "package.json")).pipe(Effect.option) - - if (Option.isSome(pkgJson)) { - const parsed = pkgJson.value as { bin?: string | Record } - if (parsed?.bin) { - const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg - const bin = parsed.bin - if (typeof bin === "string") return Option.some(unscoped) - const keys = Object.keys(bin) - if (keys.length === 1) return Option.some(keys[0]) - return bin[unscoped] ? Option.some(unscoped) : Option.some(keys[0]) - } - } - - return Option.some(files[0]) - }) - - return yield* Effect.gen(function* () { - const bin = yield* pick() - if (Option.isSome(bin)) { - return Option.some(path.join(binDir, bin.value)) - } - - yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {})) - - yield* add(pkg) - - const resolved = yield* pick() - if (Option.isNone(resolved)) return Option.none() - return Option.some(path.join(binDir, resolved.value)) - }).pipe( - Effect.scoped, - Effect.orElseSucceed(() => Option.none()), - ) - }) - - return Service.of({ - add, - install, - outdated, - which, - }) - }), - ) - - export const defaultLayer = layer.pipe( - Layer.provide(EffectFlock.layer), - Layer.provide(AppFileSystem.layer), - Layer.provide(Global.layer), - Layer.provide(NodeFileSystem.layer), - ) -} diff --git a/packages/shared/test/npm.test.ts b/packages/shared/test/npm.test.ts deleted file mode 100644 index 4443d2985c..0000000000 --- a/packages/shared/test/npm.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { Npm } from "@opencode-ai/shared/npm" - -const win = process.platform === "win32" - -describe("Npm.sanitize", () => { - test("keeps normal scoped package specs unchanged", () => { - expect(Npm.sanitize("@opencode/acme")).toBe("@opencode/acme") - expect(Npm.sanitize("@opencode/acme@1.0.0")).toBe("@opencode/acme@1.0.0") - expect(Npm.sanitize("prettier")).toBe("prettier") - }) - - test("handles git https specs", () => { - const spec = "acme@git+https://github.com/opencode/acme.git" - const expected = win ? "acme@git+https_//github.com/opencode/acme.git" : spec - expect(Npm.sanitize(spec)).toBe(expected) - }) -}) From 992435aaf8371dc784f9f3489e998e5c93451d18 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 17 Apr 2026 14:18:48 -0400 Subject: [PATCH 111/335] do not flock until reify --- packages/opencode/src/npm/effect.ts | 63 ++++++++++++++--------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/packages/opencode/src/npm/effect.ts b/packages/opencode/src/npm/effect.ts index 10b5ff179c..5968f14519 100644 --- a/packages/opencode/src/npm/effect.ts +++ b/packages/opencode/src/npm/effect.ts @@ -63,36 +63,6 @@ interface ArboristTree { edgesOut: Map } -const reify = (input: { dir: string; add?: string[] }) => - Effect.gen(function* () { - const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist")) - const arborist = new Arborist({ - path: input.dir, - binLinks: true, - progress: false, - savePrefix: "", - ignoreScripts: true, - }) - return yield* Effect.tryPromise({ - try: () => - arborist.reify({ - add: input?.add || [], - save: true, - saveType: "prod", - }), - catch: (cause) => - new InstallFailedError({ - cause, - add: input?.add, - dir: input.dir, - }), - }) as Effect.Effect - }).pipe( - Effect.withSpan("Npm.reify", { - attributes: input, - }), - ) - export const layer = Layer.effect( Service, Effect.gen(function* () { @@ -101,6 +71,36 @@ export const layer = Layer.effect( const fs = yield* FileSystem.FileSystem const flock = yield* EffectFlock.Service const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg)) + const reify = (input: { dir: string; add?: string[] }) => + Effect.gen(function* () { + yield* flock.acquire(`npm-install:${input.dir}`) + const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist")) + const arborist = new Arborist({ + path: input.dir, + binLinks: true, + progress: false, + savePrefix: "", + ignoreScripts: true, + }) + return yield* Effect.tryPromise({ + try: () => + arborist.reify({ + add: input?.add || [], + save: true, + saveType: "prod", + }), + catch: (cause) => + new InstallFailedError({ + cause, + add: input?.add, + dir: input.dir, + }), + }) as Effect.Effect + }).pipe( + Effect.withSpan("Npm.reify", { + attributes: input, + }), + ) const outdated = Effect.fn("Npm.outdated")(function* (pkg: string, cachedVersion: string) { const response = yield* Effect.tryPromise({ @@ -130,7 +130,6 @@ export const layer = Layer.effect( const add = Effect.fn("Npm.add")(function* (pkg: string) { const dir = directory(pkg) - yield* flock.acquire(`npm-install:${dir}`) const tree = yield* reify({ dir, add: [pkg] }) const first = tree.edgesOut.values().next().value?.to @@ -145,8 +144,6 @@ export const layer = Layer.effect( ) if (!canWrite) return - yield* flock.acquire(`npm-install:${dir}`) - yield* Effect.gen(function* () { const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules")) if (!nodeModulesExists) { From b1f076558cf75c9ae5322f6195e68c3c380e3f9c Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 17 Apr 2026 14:33:02 -0400 Subject: [PATCH 112/335] test: align plugin loader npm mocks - switch plugin loader tests to the effect npm module - return Option.none() for mocked npm entrypoints - keep test fixtures aligned with the current Npm.add contract --- .../cli/tui/plugin-loader-entrypoint.test.ts | 15 ++++++------ .../test/plugin/loader-shared.test.ts | 24 +++++++++---------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts b/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts index 395e8ce429..c6c25fcc11 100644 --- a/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts @@ -1,3 +1,4 @@ +import { Option } from "effect" import { expect, spyOn, test } from "bun:test" import fs from "fs/promises" import path from "path" @@ -5,7 +6,7 @@ import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui" -import { Npm } from "../../../src/npm" +import { Npm } from "../../../src/npm/effect" const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime") @@ -56,7 +57,7 @@ test("loads npm tui plugin from package ./tui export", async () => { } const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) try { await TuiPluginRuntime.init({ api: createTuiPluginApi(), config }) @@ -117,7 +118,7 @@ test("does not use npm package exports dot for tui entry", async () => { } const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) try { await TuiPluginRuntime.init({ api: createTuiPluginApi(), config }) @@ -179,7 +180,7 @@ test("rejects npm tui export that resolves outside plugin directory", async () = } const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) try { await TuiPluginRuntime.init({ api: createTuiPluginApi(), config }) @@ -241,7 +242,7 @@ test("rejects npm tui plugin that exports server and tui together", async () => } const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) try { await TuiPluginRuntime.init({ api: createTuiPluginApi(), config }) @@ -299,7 +300,7 @@ test("does not use npm package main for tui entry", async () => { } const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) const warn = spyOn(console, "warn").mockImplementation(() => {}) const error = spyOn(console, "error").mockImplementation(() => {}) @@ -468,7 +469,7 @@ test("uses npm package name when tui plugin id is omitted", async () => { } const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) try { await TuiPluginRuntime.init({ api: createTuiPluginApi(), config }) diff --git a/packages/opencode/test/plugin/loader-shared.test.ts b/packages/opencode/test/plugin/loader-shared.test.ts index 5072c1e748..8e3ad5ea0b 100644 --- a/packages/opencode/test/plugin/loader-shared.test.ts +++ b/packages/opencode/test/plugin/loader-shared.test.ts @@ -1,5 +1,5 @@ import { afterAll, afterEach, describe, expect, spyOn, test } from "bun:test" -import { Effect } from "effect" +import { Effect, Option } from "effect" import fs from "fs/promises" import path from "path" import { pathToFileURL } from "url" @@ -13,7 +13,7 @@ const { Plugin } = await import("../../src/plugin/index") const { PluginLoader } = await import("../../src/plugin/loader") const { readPackageThemes } = await import("../../src/plugin/shared") const { Instance } = await import("../../src/project/instance") -const { Npm } = await import("../../src/npm") +const { Npm } = await import("../../src/npm/effect") afterAll(() => { if (disableDefault === undefined) { @@ -239,8 +239,8 @@ describe("plugin.loader.shared", () => { }) const add = spyOn(Npm, "add").mockImplementation(async (pkg) => { - if (pkg === "acme-plugin") return { directory: tmp.extra.acme, entrypoint: tmp.extra.acme } - return { directory: tmp.extra.scope, entrypoint: tmp.extra.scope } + if (pkg === "acme-plugin") return { directory: tmp.extra.acme, entrypoint: Option.none() } + return { directory: tmp.extra.scope, entrypoint: Option.none() } }) try { @@ -301,7 +301,7 @@ describe("plugin.loader.shared", () => { }, }) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) try { await load(tmp.path) @@ -358,7 +358,7 @@ describe("plugin.loader.shared", () => { }, }) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) try { await load(tmp.path) @@ -410,7 +410,7 @@ describe("plugin.loader.shared", () => { }, }) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) try { await load(tmp.path) @@ -455,7 +455,7 @@ describe("plugin.loader.shared", () => { }, }) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) try { await load(tmp.path) @@ -518,7 +518,7 @@ describe("plugin.loader.shared", () => { }, }) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) try { await load(tmp.path) @@ -548,7 +548,7 @@ describe("plugin.loader.shared", () => { }, }) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: "", entrypoint: "" }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: "", entrypoint: Option.none() }) try { await load(tmp.path) @@ -927,7 +927,7 @@ export default { }, }) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) const missing: string[] = [] try { @@ -996,7 +996,7 @@ export default { }, }) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) try { const loaded = await PluginLoader.loadExternal({ From bbb422d1250da1400c9c228d363bebb336e238ca Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:47:22 -0500 Subject: [PATCH 113/335] chore: bump ai to 6.0.168 and @ai-sdk/gateway to 3.0.104 (#23145) --- bun.lock | 12 +++++------- package.json | 2 +- packages/opencode/package.json | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/bun.lock b/bun.lock index c78c9ae48d..8d83d8ab47 100644 --- a/bun.lock +++ b/bun.lock @@ -328,7 +328,7 @@ "@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", - "@ai-sdk/gateway": "3.0.102", + "@ai-sdk/gateway": "3.0.104", "@ai-sdk/google": "3.0.63", "@ai-sdk/google-vertex": "4.0.112", "@ai-sdk/groq": "3.0.31", @@ -692,7 +692,7 @@ "@types/node": "22.13.9", "@types/semver": "7.7.1", "@typescript/native-preview": "7.0.0-dev.20251207.1", - "ai": "6.0.158", + "ai": "6.0.168", "cross-spawn": "7.0.6", "diff": "8.0.2", "dompurify": "3.3.1", @@ -760,7 +760,7 @@ "@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.46", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XRKR0zgRyegdmtK5CDUEjlyRp0Fo+XVCdoG+301U1SGtgRIAYG3ObVtgzVJBVpJdHFSLHuYeLTnNiQoUxD7+FQ=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.102", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-GrwDpaYJiVafrsA1MTbZtXPcQUI67g5AXiJo7Y1F8b+w+SiYHLk3ZIn1YmpQVoVAh2bjvxjj+Vo0AvfskuGH4g=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="], "@ai-sdk/google": ["@ai-sdk/google@3.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ=="], @@ -2456,7 +2456,7 @@ "@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="], - "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], + "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], @@ -2516,7 +2516,7 @@ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "ai": ["ai@6.0.158", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gLTp1UXFtMqKUi3XHs33K7UFglbvojkxF/aq337TxnLGOhHIW9+GyP2jwW4hYX87f1es+wId3VQoPRRu9zEStQ=="], + "ai": ["ai@6.0.168", "", { "dependencies": { "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ=="], "ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="], @@ -5702,8 +5702,6 @@ "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - "ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="], - "ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="], "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="], diff --git a/package.json b/package.json index f1ad03afc8..ddd711adaf 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "drizzle-kit": "1.0.0-beta.19-d95b7a4", "drizzle-orm": "1.0.0-beta.19-d95b7a4", "effect": "4.0.0-beta.48", - "ai": "6.0.158", + "ai": "6.0.168", "cross-spawn": "7.0.6", "hono": "4.10.7", "hono-openapi": "1.1.2", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index cadb1fa38d..2f56b74775 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -85,7 +85,7 @@ "@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", - "@ai-sdk/gateway": "3.0.102", + "@ai-sdk/gateway": "3.0.104", "@ai-sdk/google": "3.0.63", "@ai-sdk/google-vertex": "4.0.112", "@ai-sdk/groq": "3.0.31", From 467be08e679d82c20164870f067eb759abe5f6ec Mon Sep 17 00:00:00 2001 From: Dax Date: Fri, 17 Apr 2026 14:58:37 -0400 Subject: [PATCH 114/335] refactor: consolidate npm exports and trace flock acquisition (#23151) --- .../opencode/src/cli/cmd/tui/config/tui.ts | 2 +- packages/opencode/src/cli/cmd/tui/layer.ts | 2 +- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/effect/app-runtime.ts | 2 +- packages/opencode/src/npm/effect.ts | 258 ----------- packages/opencode/src/npm/index.ts | 405 +++++++++++------- packages/opencode/src/plugin/shared.ts | 2 +- .../cli/tui/plugin-loader-entrypoint.test.ts | 15 +- packages/opencode/test/config/config.test.ts | 2 +- .../test/plugin/loader-shared.test.ts | 24 +- packages/shared/src/util/effect-flock.ts | 75 ++-- 11 files changed, 304 insertions(+), 485 deletions(-) delete mode 100644 packages/opencode/src/npm/effect.ts diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index abcf11fcef..179046e026 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -18,7 +18,7 @@ import { InstallationLocal, InstallationVersion } from "@/installation/version" import { makeRuntime } from "@/effect/runtime" import { Filesystem, Log } from "@/util" import { ConfigVariable } from "@/config/variable" -import { Npm } from "@/npm/effect" +import { Npm } from "@/npm" const log = Log.create({ service: "tui.config" }) diff --git a/packages/opencode/src/cli/cmd/tui/layer.ts b/packages/opencode/src/cli/cmd/tui/layer.ts index 66497f8b1a..64cba08e82 100644 --- a/packages/opencode/src/cli/cmd/tui/layer.ts +++ b/packages/opencode/src/cli/cmd/tui/layer.ts @@ -1,6 +1,6 @@ import { Layer } from "effect" import { TuiConfig } from "./config/tui" -import { Npm } from "@/npm/effect" +import { Npm } from "@/npm" import { Observability } from "@/effect/observability" export const CliLayer = Observability.layer.pipe(Layer.merge(TuiConfig.layer), Layer.provide(Npm.defaultLayer)) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 8980765b79..459f76961a 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -38,7 +38,7 @@ import { ConfigPaths } from "./paths" import { ConfigFormatter } from "./formatter" import { ConfigLSP } from "./lsp" import { ConfigVariable } from "./variable" -import { Npm } from "@/npm/effect" +import { Npm } from "@/npm" const log = Log.create({ service: "config" }) diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index 262d85e7ea..d68e00a323 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -46,7 +46,7 @@ import { Pty } from "@/pty" import { Installation } from "@/installation" import { ShareNext } from "@/share" import { SessionShare } from "@/share" -import { Npm } from "@/npm/effect" +import { Npm } from "@/npm" import { memoMap } from "./memo-map" export const AppLayer = Layer.mergeAll( diff --git a/packages/opencode/src/npm/effect.ts b/packages/opencode/src/npm/effect.ts deleted file mode 100644 index 5968f14519..0000000000 --- a/packages/opencode/src/npm/effect.ts +++ /dev/null @@ -1,258 +0,0 @@ -export * as Npm from "./effect" - -import path from "path" -import semver from "semver" -import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect" -import { NodeFileSystem } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Global } from "@opencode-ai/shared/global" -import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" - -import { makeRuntime } from "../effect/runtime" - -export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", { - add: Schema.Array(Schema.String).pipe(Schema.optional), - dir: Schema.String, - cause: Schema.optional(Schema.Defect), -}) {} - -export interface EntryPoint { - readonly directory: string - readonly entrypoint: Option.Option -} - -export interface Interface { - readonly add: (pkg: string) => Effect.Effect - readonly install: ( - dir: string, - input?: { add: string[] }, - ) => Effect.Effect - readonly outdated: (pkg: string, cachedVersion: string) => Effect.Effect - readonly which: (pkg: string) => Effect.Effect> -} - -export class Service extends Context.Service()("@opencode/Npm") {} - -const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined - -export function sanitize(pkg: string) { - if (!illegal) return pkg - return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("") -} - -const resolveEntryPoint = (name: string, dir: string): EntryPoint => { - let entrypoint: Option.Option - try { - const resolved = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir) - entrypoint = Option.some(resolved) - } catch { - entrypoint = Option.none() - } - return { - directory: dir, - entrypoint, - } -} - -interface ArboristNode { - name: string - path: string -} - -interface ArboristTree { - edgesOut: Map -} - -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const afs = yield* AppFileSystem.Service - const global = yield* Global.Service - const fs = yield* FileSystem.FileSystem - const flock = yield* EffectFlock.Service - const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg)) - const reify = (input: { dir: string; add?: string[] }) => - Effect.gen(function* () { - yield* flock.acquire(`npm-install:${input.dir}`) - const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist")) - const arborist = new Arborist({ - path: input.dir, - binLinks: true, - progress: false, - savePrefix: "", - ignoreScripts: true, - }) - return yield* Effect.tryPromise({ - try: () => - arborist.reify({ - add: input?.add || [], - save: true, - saveType: "prod", - }), - catch: (cause) => - new InstallFailedError({ - cause, - add: input?.add, - dir: input.dir, - }), - }) as Effect.Effect - }).pipe( - Effect.withSpan("Npm.reify", { - attributes: input, - }), - ) - - const outdated = Effect.fn("Npm.outdated")(function* (pkg: string, cachedVersion: string) { - const response = yield* Effect.tryPromise({ - try: () => fetch(`https://registry.npmjs.org/${pkg}`), - catch: () => undefined, - }).pipe(Effect.orElseSucceed(() => undefined)) - - if (!response || !response.ok) { - return false - } - - const data = yield* Effect.tryPromise({ - try: () => response.json() as Promise<{ "dist-tags"?: { latest?: string } }>, - catch: () => undefined, - }).pipe(Effect.orElseSucceed(() => undefined)) - - const latestVersion = data?.["dist-tags"]?.latest - if (!latestVersion) { - return false - } - - const range = /[\s^~*xX<>|=]/.test(cachedVersion) - if (range) return !semver.satisfies(latestVersion, cachedVersion) - - return semver.lt(cachedVersion, latestVersion) - }) - - const add = Effect.fn("Npm.add")(function* (pkg: string) { - const dir = directory(pkg) - - const tree = yield* reify({ dir, add: [pkg] }) - const first = tree.edgesOut.values().next().value?.to - if (!first) return yield* new InstallFailedError({ add: [pkg], dir }) - return resolveEntryPoint(first.name, first.path) - }, Effect.scoped) - - const install = Effect.fn("Npm.install")(function* (dir: string, input?: { add: string[] }) { - const canWrite = yield* afs.access(dir, { writable: true }).pipe( - Effect.as(true), - Effect.orElseSucceed(() => false), - ) - if (!canWrite) return - - yield* Effect.gen(function* () { - const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules")) - if (!nodeModulesExists) { - yield* reify({ add: input?.add, dir }) - return - } - }).pipe(Effect.withSpan("Npm.checkNodeModules")) - - yield* Effect.gen(function* () { - const pkg = yield* afs.readJson(path.join(dir, "package.json")).pipe(Effect.orElseSucceed(() => ({}))) - const lock = yield* afs.readJson(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => ({}))) - - const pkgAny = pkg as any - const lockAny = lock as any - const declared = new Set([ - ...Object.keys(pkgAny?.dependencies || {}), - ...Object.keys(pkgAny?.devDependencies || {}), - ...Object.keys(pkgAny?.peerDependencies || {}), - ...Object.keys(pkgAny?.optionalDependencies || {}), - ...(input?.add || []), - ]) - - const root = lockAny?.packages?.[""] || {} - const locked = new Set([ - ...Object.keys(root?.dependencies || {}), - ...Object.keys(root?.devDependencies || {}), - ...Object.keys(root?.peerDependencies || {}), - ...Object.keys(root?.optionalDependencies || {}), - ]) - - for (const name of declared) { - if (!locked.has(name)) { - yield* reify({ dir, add: input?.add }) - return - } - } - }).pipe(Effect.withSpan("Npm.checkDirty")) - - return - }, Effect.scoped) - - const which = Effect.fn("Npm.which")(function* (pkg: string) { - const dir = directory(pkg) - const binDir = path.join(dir, "node_modules", ".bin") - - const pick = Effect.fnUntraced(function* () { - const files = yield* fs.readDirectory(binDir).pipe(Effect.catch(() => Effect.succeed([] as string[]))) - - if (files.length === 0) return Option.none() - if (files.length === 1) return Option.some(files[0]) - - const pkgJson = yield* afs.readJson(path.join(dir, "node_modules", pkg, "package.json")).pipe(Effect.option) - - if (Option.isSome(pkgJson)) { - const parsed = pkgJson.value as { bin?: string | Record } - if (parsed?.bin) { - const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg - const bin = parsed.bin - if (typeof bin === "string") return Option.some(unscoped) - const keys = Object.keys(bin) - if (keys.length === 1) return Option.some(keys[0]) - return bin[unscoped] ? Option.some(unscoped) : Option.some(keys[0]) - } - } - - return Option.some(files[0]) - }) - - return yield* Effect.gen(function* () { - const bin = yield* pick() - if (Option.isSome(bin)) { - return Option.some(path.join(binDir, bin.value)) - } - - yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {})) - - yield* add(pkg) - - const resolved = yield* pick() - if (Option.isNone(resolved)) return Option.none() - return Option.some(path.join(binDir, resolved.value)) - }).pipe( - Effect.scoped, - Effect.orElseSucceed(() => Option.none()), - ) - }) - - return Service.of({ - add, - install, - outdated, - which, - }) - }), -) - -export const defaultLayer = layer.pipe( - Layer.provide(EffectFlock.layer), - Layer.provide(AppFileSystem.layer), - Layer.provide(Global.layer), - Layer.provide(NodeFileSystem.layer), -) - -const { runPromise } = makeRuntime(Service, defaultLayer) - -export async function install(...args: Parameters) { - return runPromise((svc) => svc.install(...args)) -} - -export async function add(...args: Parameters) { - return runPromise((svc) => svc.add(...args)) -} diff --git a/packages/opencode/src/npm/index.ts b/packages/opencode/src/npm/index.ts index 425b27f420..f242598192 100644 --- a/packages/opencode/src/npm/index.ts +++ b/packages/opencode/src/npm/index.ts @@ -1,198 +1,271 @@ -import semver from "semver" -import z from "zod" -import { NamedError } from "@opencode-ai/shared/util/error" -import { Global } from "../global" -import { Log } from "../util" +export * as Npm from "." + import path from "path" -import { readdir, rm } from "fs/promises" -import { Filesystem } from "@/util" -import { Flock } from "@opencode-ai/shared/util/flock" +import semver from "semver" +import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect" +import { NodeFileSystem } from "@effect/platform-node" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { Global } from "@opencode-ai/shared/global" +import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" + +import { makeRuntime } from "../effect/runtime" + +export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", { + add: Schema.Array(Schema.String).pipe(Schema.optional), + dir: Schema.String, + cause: Schema.optional(Schema.Defect), +}) {} + +export interface EntryPoint { + readonly directory: string + readonly entrypoint: Option.Option +} + +export interface Interface { + readonly add: (pkg: string) => Effect.Effect + readonly install: ( + dir: string, + input?: { add: string[] }, + ) => Effect.Effect + readonly outdated: (pkg: string, cachedVersion: string) => Effect.Effect + readonly which: (pkg: string) => Effect.Effect> +} + +export class Service extends Context.Service()("@opencode/Npm") {} -const log = Log.create({ service: "npm" }) const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined -export const InstallFailedError = NamedError.create( - "NpmInstallFailedError", - z.object({ - pkg: z.string(), - }), -) - export function sanitize(pkg: string) { if (!illegal) return pkg return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("") } -function directory(pkg: string) { - return path.join(Global.Path.cache, "packages", sanitize(pkg)) -} - -function resolveEntryPoint(name: string, dir: string) { - let entrypoint: string | undefined +const resolveEntryPoint = (name: string, dir: string): EntryPoint => { + let entrypoint: Option.Option try { - entrypoint = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir) - } catch {} - const result = { + const resolved = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir) + entrypoint = Option.some(resolved) + } catch { + entrypoint = Option.none() + } + return { directory: dir, entrypoint, } - return result } -export async function outdated(pkg: string, cachedVersion: string): Promise { - const response = await fetch(`https://registry.npmjs.org/${pkg}`) - if (!response.ok) { - log.warn("Failed to resolve latest version, using cached", { pkg, cachedVersion }) - return false - } - - const data = (await response.json()) as { "dist-tags"?: { latest?: string } } - const latestVersion = data?.["dist-tags"]?.latest - if (!latestVersion) { - log.warn("No latest version found, using cached", { pkg, cachedVersion }) - return false - } - - const range = /[\s^~*xX<>|=]/.test(cachedVersion) - if (range) return !semver.satisfies(latestVersion, cachedVersion) - - return semver.lt(cachedVersion, latestVersion) +interface ArboristNode { + name: string + path: string } -export async function add(pkg: string) { - const { Arborist } = await import("@npmcli/arborist") - const dir = directory(pkg) - await using _ = await Flock.acquire(`npm-install:${Filesystem.resolve(dir)}`) - log.info("installing package", { - pkg, - }) +interface ArboristTree { + edgesOut: Map +} - const arborist = new Arborist({ - path: dir, - binLinks: true, - progress: false, - savePrefix: "", - ignoreScripts: true, - }) - const tree = await arborist.loadVirtual().catch(() => {}) - if (tree) { - const first = tree.edgesOut.values().next().value?.to - if (first) { - return resolveEntryPoint(first.name, first.path) - } - } +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const afs = yield* AppFileSystem.Service + const global = yield* Global.Service + const fs = yield* FileSystem.FileSystem + const flock = yield* EffectFlock.Service + const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg)) + const reify = (input: { dir: string; add?: string[] }) => + Effect.gen(function* () { + yield* flock.acquire(`npm-install:${input.dir}`) + const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist")) + const arborist = new Arborist({ + path: input.dir, + binLinks: true, + progress: false, + savePrefix: "", + ignoreScripts: true, + }) + return yield* Effect.tryPromise({ + try: () => + arborist.reify({ + add: input?.add || [], + save: true, + saveType: "prod", + }), + catch: (cause) => + new InstallFailedError({ + cause, + add: input?.add, + dir: input.dir, + }), + }) as Effect.Effect + }).pipe( + Effect.withSpan("Npm.reify", { + attributes: input, + }), + ) - const result = await arborist - .reify({ - add: [pkg], - save: true, - saveType: "prod", + const outdated = Effect.fn("Npm.outdated")(function* (pkg: string, cachedVersion: string) { + const response = yield* Effect.tryPromise({ + try: () => fetch(`https://registry.npmjs.org/${pkg}`), + catch: () => undefined, + }).pipe(Effect.orElseSucceed(() => undefined)) + + if (!response || !response.ok) { + return false + } + + const data = yield* Effect.tryPromise({ + try: () => response.json() as Promise<{ "dist-tags"?: { latest?: string } }>, + catch: () => undefined, + }).pipe(Effect.orElseSucceed(() => undefined)) + + const latestVersion = data?.["dist-tags"]?.latest + if (!latestVersion) { + return false + } + + const range = /[\s^~*xX<>|=]/.test(cachedVersion) + if (range) return !semver.satisfies(latestVersion, cachedVersion) + + return semver.lt(cachedVersion, latestVersion) }) - .catch((cause) => { - throw new InstallFailedError( - { pkg }, - { - cause, - }, + + const add = Effect.fn("Npm.add")(function* (pkg: string) { + const dir = directory(pkg) + + const tree = yield* reify({ dir, add: [pkg] }) + const first = tree.edgesOut.values().next().value?.to + if (!first) return yield* new InstallFailedError({ add: [pkg], dir }) + return resolveEntryPoint(first.name, first.path) + }, Effect.scoped) + + const install = Effect.fn("Npm.install")(function* (dir: string, input?: { add: string[] }) { + const canWrite = yield* afs.access(dir, { writable: true }).pipe( + Effect.as(true), + Effect.orElseSucceed(() => false), + ) + if (!canWrite) return + + yield* Effect.gen(function* () { + const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules")) + if (!nodeModulesExists) { + yield* reify({ add: input?.add, dir }) + return + } + }).pipe(Effect.withSpan("Npm.checkNodeModules")) + + yield* Effect.gen(function* () { + const pkg = yield* afs.readJson(path.join(dir, "package.json")).pipe(Effect.orElseSucceed(() => ({}))) + const lock = yield* afs.readJson(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => ({}))) + + const pkgAny = pkg as any + const lockAny = lock as any + const declared = new Set([ + ...Object.keys(pkgAny?.dependencies || {}), + ...Object.keys(pkgAny?.devDependencies || {}), + ...Object.keys(pkgAny?.peerDependencies || {}), + ...Object.keys(pkgAny?.optionalDependencies || {}), + ...(input?.add || []), + ]) + + const root = lockAny?.packages?.[""] || {} + const locked = new Set([ + ...Object.keys(root?.dependencies || {}), + ...Object.keys(root?.devDependencies || {}), + ...Object.keys(root?.peerDependencies || {}), + ...Object.keys(root?.optionalDependencies || {}), + ]) + + for (const name of declared) { + if (!locked.has(name)) { + yield* reify({ dir, add: input?.add }) + return + } + } + }).pipe(Effect.withSpan("Npm.checkDirty")) + + return + }, Effect.scoped) + + const which = Effect.fn("Npm.which")(function* (pkg: string) { + const dir = directory(pkg) + const binDir = path.join(dir, "node_modules", ".bin") + + const pick = Effect.fnUntraced(function* () { + const files = yield* fs.readDirectory(binDir).pipe(Effect.catch(() => Effect.succeed([] as string[]))) + + if (files.length === 0) return Option.none() + if (files.length === 1) return Option.some(files[0]) + + const pkgJson = yield* afs.readJson(path.join(dir, "node_modules", pkg, "package.json")).pipe(Effect.option) + + if (Option.isSome(pkgJson)) { + const parsed = pkgJson.value as { bin?: string | Record } + if (parsed?.bin) { + const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg + const bin = parsed.bin + if (typeof bin === "string") return Option.some(unscoped) + const keys = Object.keys(bin) + if (keys.length === 1) return Option.some(keys[0]) + return bin[unscoped] ? Option.some(unscoped) : Option.some(keys[0]) + } + } + + return Option.some(files[0]) + }) + + return yield* Effect.gen(function* () { + const bin = yield* pick() + if (Option.isSome(bin)) { + return Option.some(path.join(binDir, bin.value)) + } + + yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {})) + + yield* add(pkg) + + const resolved = yield* pick() + if (Option.isNone(resolved)) return Option.none() + return Option.some(path.join(binDir, resolved.value)) + }).pipe( + Effect.scoped, + Effect.orElseSucceed(() => Option.none()), ) }) - const first = result.edgesOut.values().next().value?.to - if (!first) throw new InstallFailedError({ pkg }) - return resolveEntryPoint(first.name, first.path) -} - -export async function install(dir: string) { - await using _ = await Flock.acquire(`npm-install:${dir}`) - log.info("checking dependencies", { dir }) - - const reify = async () => { - const { Arborist } = await import("@npmcli/arborist") - const arb = new Arborist({ - path: dir, - binLinks: true, - progress: false, - savePrefix: "", - ignoreScripts: true, + return Service.of({ + add, + install, + outdated, + which, }) - await arb.reify().catch(() => {}) - } + }), +) - if (!(await Filesystem.exists(path.join(dir, "node_modules")))) { - log.info("node_modules missing, reifying") - await reify() - return - } +export const defaultLayer = layer.pipe( + Layer.provide(EffectFlock.layer), + Layer.provide(AppFileSystem.layer), + Layer.provide(Global.layer), + Layer.provide(NodeFileSystem.layer), +) - type PackageDeps = Record - type PackageJson = { - dependencies?: PackageDeps - devDependencies?: PackageDeps - peerDependencies?: PackageDeps - optionalDependencies?: PackageDeps - } - const pkg: PackageJson = await Filesystem.readJson(path.join(dir, "package.json")).catch(() => ({})) - const lock: { packages?: Record } = await Filesystem.readJson<{ - packages?: Record - }>(path.join(dir, "package-lock.json")).catch(() => ({})) +const { runPromise } = makeRuntime(Service, defaultLayer) - const declared = new Set([ - ...Object.keys(pkg.dependencies || {}), - ...Object.keys(pkg.devDependencies || {}), - ...Object.keys(pkg.peerDependencies || {}), - ...Object.keys(pkg.optionalDependencies || {}), - ]) - - const root = lock.packages?.[""] || {} - const locked = new Set([ - ...Object.keys(root.dependencies || {}), - ...Object.keys(root.devDependencies || {}), - ...Object.keys(root.peerDependencies || {}), - ...Object.keys(root.optionalDependencies || {}), - ]) - - for (const name of declared) { - if (!locked.has(name)) { - log.info("dependency not in lock file, reifying", { name }) - await reify() - return - } - } - - log.info("dependencies in sync") +export async function install(...args: Parameters) { + return runPromise((svc) => svc.install(...args)) } -export async function which(pkg: string) { - const dir = directory(pkg) - const binDir = path.join(dir, "node_modules", ".bin") - - const pick = async () => { - const files = await readdir(binDir).catch(() => []) - if (files.length === 0) return undefined - if (files.length === 1) return files[0] - // Multiple binaries — resolve from package.json bin field like npx does - const pkgJson = await Filesystem.readJson<{ bin?: string | Record }>( - path.join(dir, "node_modules", pkg, "package.json"), - ).catch(() => undefined) - if (pkgJson?.bin) { - const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg - const bin = pkgJson.bin - if (typeof bin === "string") return unscoped - const keys = Object.keys(bin) - if (keys.length === 1) return keys[0] - return bin[unscoped] ? unscoped : keys[0] - } - return files[0] +export async function add(...args: Parameters) { + const entry = await runPromise((svc) => svc.add(...args)) + return { + directory: entry.directory, + entrypoint: Option.getOrUndefined(entry.entrypoint), } - - const bin = await pick() - if (bin) return path.join(binDir, bin) - - await rm(path.join(dir, "package-lock.json"), { force: true }) - await add(pkg) - const resolved = await pick() - if (!resolved) return - return path.join(binDir, resolved) } -export * as Npm from "." +export async function outdated(...args: Parameters) { + return runPromise((svc) => svc.outdated(...args)) +} + +export async function which(...args: Parameters) { + const resolved = await runPromise((svc) => svc.which(...args)) + return Option.getOrUndefined(resolved) +} diff --git a/packages/opencode/src/plugin/shared.ts b/packages/opencode/src/plugin/shared.ts index f431204fc4..ca821216d4 100644 --- a/packages/opencode/src/plugin/shared.ts +++ b/packages/opencode/src/plugin/shared.ts @@ -4,7 +4,7 @@ import npa from "npm-package-arg" import semver from "semver" import { Filesystem } from "@/util" import { isRecord } from "@/util/record" -import { Npm } from "@/npm/effect" +import { Npm } from "@/npm" // Old npm package names for plugins that are now built-in export const DEPRECATED_PLUGIN_PACKAGES = ["opencode-openai-codex-auth", "opencode-copilot-auth"] diff --git a/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts b/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts index c6c25fcc11..74236afae8 100644 --- a/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts @@ -1,4 +1,3 @@ -import { Option } from "effect" import { expect, spyOn, test } from "bun:test" import fs from "fs/promises" import path from "path" @@ -6,7 +5,7 @@ import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui" -import { Npm } from "../../../src/npm/effect" +import { Npm } from "../../../src/npm" const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime") @@ -57,7 +56,7 @@ test("loads npm tui plugin from package ./tui export", async () => { } const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) try { await TuiPluginRuntime.init({ api: createTuiPluginApi(), config }) @@ -118,7 +117,7 @@ test("does not use npm package exports dot for tui entry", async () => { } const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) try { await TuiPluginRuntime.init({ api: createTuiPluginApi(), config }) @@ -180,7 +179,7 @@ test("rejects npm tui export that resolves outside plugin directory", async () = } const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) try { await TuiPluginRuntime.init({ api: createTuiPluginApi(), config }) @@ -242,7 +241,7 @@ test("rejects npm tui plugin that exports server and tui together", async () => } const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) try { await TuiPluginRuntime.init({ api: createTuiPluginApi(), config }) @@ -300,7 +299,7 @@ test("does not use npm package main for tui entry", async () => { } const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) const warn = spyOn(console, "warn").mockImplementation(() => {}) const error = spyOn(console, "error").mockImplementation(() => {}) @@ -469,7 +468,7 @@ test("uses npm package name when tui plugin id is omitted", async () => { } const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) try { await TuiPluginRuntime.init({ api: createTuiPluginApi(), config }) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 7b01ee626a..9f2bf9db9a 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -27,7 +27,7 @@ import { Global } from "../../src/global" import { ProjectID } from "../../src/project/schema" import { Filesystem } from "../../src/util" import { ConfigPlugin } from "@/config/plugin" -import { Npm } from "@/npm/effect" +import { Npm } from "@/npm" const emptyAccount = Layer.mock(Account.Service)({ active: () => Effect.succeed(Option.none()), diff --git a/packages/opencode/test/plugin/loader-shared.test.ts b/packages/opencode/test/plugin/loader-shared.test.ts index 8e3ad5ea0b..83e9d71b4f 100644 --- a/packages/opencode/test/plugin/loader-shared.test.ts +++ b/packages/opencode/test/plugin/loader-shared.test.ts @@ -1,5 +1,5 @@ import { afterAll, afterEach, describe, expect, spyOn, test } from "bun:test" -import { Effect, Option } from "effect" +import { Effect } from "effect" import fs from "fs/promises" import path from "path" import { pathToFileURL } from "url" @@ -13,7 +13,7 @@ const { Plugin } = await import("../../src/plugin/index") const { PluginLoader } = await import("../../src/plugin/loader") const { readPackageThemes } = await import("../../src/plugin/shared") const { Instance } = await import("../../src/project/instance") -const { Npm } = await import("../../src/npm/effect") +const { Npm } = await import("../../src/npm") afterAll(() => { if (disableDefault === undefined) { @@ -239,8 +239,8 @@ describe("plugin.loader.shared", () => { }) const add = spyOn(Npm, "add").mockImplementation(async (pkg) => { - if (pkg === "acme-plugin") return { directory: tmp.extra.acme, entrypoint: Option.none() } - return { directory: tmp.extra.scope, entrypoint: Option.none() } + if (pkg === "acme-plugin") return { directory: tmp.extra.acme, entrypoint: undefined } + return { directory: tmp.extra.scope, entrypoint: undefined } }) try { @@ -301,7 +301,7 @@ describe("plugin.loader.shared", () => { }, }) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) try { await load(tmp.path) @@ -358,7 +358,7 @@ describe("plugin.loader.shared", () => { }, }) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) try { await load(tmp.path) @@ -410,7 +410,7 @@ describe("plugin.loader.shared", () => { }, }) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) try { await load(tmp.path) @@ -455,7 +455,7 @@ describe("plugin.loader.shared", () => { }, }) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) try { await load(tmp.path) @@ -518,7 +518,7 @@ describe("plugin.loader.shared", () => { }, }) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) try { await load(tmp.path) @@ -548,7 +548,7 @@ describe("plugin.loader.shared", () => { }, }) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: "", entrypoint: Option.none() }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: "", entrypoint: undefined }) try { await load(tmp.path) @@ -927,7 +927,7 @@ export default { }, }) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) const missing: string[] = [] try { @@ -996,7 +996,7 @@ export default { }, }) - const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: Option.none() }) + const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) try { const loaded = await PluginLoader.loadExternal({ diff --git a/packages/shared/src/util/effect-flock.ts b/packages/shared/src/util/effect-flock.ts index 3e00afc9e4..16bcf091b4 100644 --- a/packages/shared/src/util/effect-flock.ts +++ b/packages/shared/src/util/effect-flock.ts @@ -165,55 +165,60 @@ export namespace EffectFlock { type Handle = { token: string; metaPath: string; heartbeatPath: string; lockDir: string } - const tryAcquireLockDir = Effect.fn("EffectFlock.tryAcquire")(function* (lockDir: string) { - const token = randomUUID() - const metaPath = path.join(lockDir, "meta.json") - const heartbeatPath = path.join(lockDir, "heartbeat") + const tryAcquireLockDir = (lockDir: string, key: string) => + Effect.gen(function* () { + const token = randomUUID() + const metaPath = path.join(lockDir, "meta.json") + const heartbeatPath = path.join(lockDir, "heartbeat") - // Atomic mkdir — the POSIX lock primitive - const created = yield* atomicMkdir(lockDir) + // Atomic mkdir — the POSIX lock primitive + const created = yield* atomicMkdir(lockDir) - if (!created) { - if (!(yield* isStale(lockDir, heartbeatPath, metaPath))) return yield* new NotAcquired() + if (!created) { + if (!(yield* isStale(lockDir, heartbeatPath, metaPath))) return yield* new NotAcquired() - // Stale — race for breaker ownership - const breakerPath = lockDir + ".breaker" + // Stale — race for breaker ownership + const breakerPath = lockDir + ".breaker" - const claimed = yield* fs.makeDirectory(breakerPath, { mode: 0o700 }).pipe( - Effect.as(true), - Effect.catchIf( - (e) => e.reason._tag === "AlreadyExists", - () => cleanStaleBreaker(breakerPath), - ), - Effect.catchIf(isPathGone, () => Effect.succeed(false)), - Effect.orDie, - ) + const claimed = yield* fs.makeDirectory(breakerPath, { mode: 0o700 }).pipe( + Effect.as(true), + Effect.catchIf( + (e) => e.reason._tag === "AlreadyExists", + () => cleanStaleBreaker(breakerPath), + ), + Effect.catchIf(isPathGone, () => Effect.succeed(false)), + Effect.orDie, + ) - if (!claimed) return yield* new NotAcquired() + if (!claimed) return yield* new NotAcquired() - // We own the breaker — double-check staleness, nuke, recreate - const recreated = yield* Effect.gen(function* () { - if (!(yield* isStale(lockDir, heartbeatPath, metaPath))) return false - yield* forceRemove(lockDir) - return yield* atomicMkdir(lockDir) - }).pipe(Effect.ensuring(forceRemove(breakerPath))) + // We own the breaker — double-check staleness, nuke, recreate + const recreated = yield* Effect.gen(function* () { + if (!(yield* isStale(lockDir, heartbeatPath, metaPath))) return false + yield* forceRemove(lockDir) + return yield* atomicMkdir(lockDir) + }).pipe(Effect.ensuring(forceRemove(breakerPath))) - if (!recreated) return yield* new NotAcquired() - } + if (!recreated) return yield* new NotAcquired() + } - // We own the lock dir — write heartbeat + meta with exclusive create - yield* exclusiveWrite(heartbeatPath, "", lockDir, "heartbeat already existed") + // We own the lock dir — write heartbeat + meta with exclusive create + yield* exclusiveWrite(heartbeatPath, "", lockDir, "heartbeat already existed") - const metaJson = encodeMeta({ token, pid: process.pid, hostname, createdAt: new Date().toISOString() }) - yield* exclusiveWrite(metaPath, metaJson, lockDir, "meta.json already existed") + const metaJson = encodeMeta({ token, pid: process.pid, hostname, createdAt: new Date().toISOString() }) + yield* exclusiveWrite(metaPath, metaJson, lockDir, "meta.json already existed") - return { token, metaPath, heartbeatPath, lockDir } satisfies Handle - }) + return { token, metaPath, heartbeatPath, lockDir } satisfies Handle + }).pipe( + Effect.withSpan("EffectFlock.tryAcquire", { + attributes: { key }, + }), + ) // -- retry wrapper (preserves Handle type) -- const acquireHandle = (lockfile: string, key: string): Effect.Effect => - tryAcquireLockDir(lockfile).pipe( + tryAcquireLockDir(lockfile, key).pipe( Effect.retry({ while: (err) => err._tag === "NotAcquired", schedule: retrySchedule, From b275b8580d7bd3b884cf535e28c1b826759eb14b Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 17 Apr 2026 15:14:05 -0400 Subject: [PATCH 115/335] feat(tui): minor UX improvements for workspaces (#23146) --- .../cmd/tui/component/dialog-session-list.tsx | 9 +- .../tui/component/dialog-workspace-create.tsx | 11 ++- .../dialog-workspace-unavailable.tsx | 83 +++++++++++++++++++ .../cli/cmd/tui/component/prompt/index.tsx | 53 ++++++++++-- .../cli/cmd/tui/routes/session/sidebar.tsx | 20 +++++ 5 files changed, 159 insertions(+), 17 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 60ef6087ba..32342e7724 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -139,15 +139,10 @@ export function DialogSessionList() { {desc}{" "} - ■ + ● ) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index 7ea513edee..6dcdabe0b9 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -139,7 +139,16 @@ export async function restoreWorkspaceSession(input: { total: result.data.total, }) - await Promise.all([input.project.workspace.sync(), input.sync.session.refresh()]).catch((err) => { + input.project.workspace.set(input.workspaceID) + + try { + await input.sync.bootstrap({ fatal: false }) + } catch (e) {} + + await Promise.all([ + input.project.workspace.sync(), + input.sync.session.sync(input.sessionID), + ]).catch((err) => { log.error("session restore refresh failed", { workspaceID: input.workspaceID, sessionID: input.sessionID, diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx new file mode 100644 index 0000000000..0c2dd3e2f3 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx @@ -0,0 +1,83 @@ +import { TextAttributes } from "@opentui/core" +import { useKeyboard } from "@opentui/solid" +import { createStore } from "solid-js/store" +import { For } from "solid-js" +import { useTheme } from "../context/theme" +import { useDialog } from "../ui/dialog" + +export function DialogWorkspaceUnavailable(props: { + onRestore?: () => boolean | void | Promise +}) { + const dialog = useDialog() + const { theme } = useTheme() + const [store, setStore] = createStore({ + active: "restore" as "cancel" | "restore", + }) + + const options = ["cancel", "restore"] as const + + async function confirm() { + if (store.active === "cancel") { + dialog.clear() + return + } + const result = await props.onRestore?.() + if (result === false) return + } + + useKeyboard((evt) => { + if (evt.name === "return") { + evt.preventDefault() + evt.stopPropagation() + void confirm() + return + } + if (evt.name === "left") { + evt.preventDefault() + evt.stopPropagation() + setStore("active", "cancel") + return + } + if (evt.name === "right") { + evt.preventDefault() + evt.stopPropagation() + setStore("active", "restore") + } + }) + + return ( + + + + Workspace Unavailable + + dialog.clear()}> + esc + + + + This session is attached to a workspace that is no longer available. + + + Would you like to restore this session into a new workspace? + + + + {(item) => ( + { + setStore("active", item) + void confirm() + }} + > + {item} + + )} + + + + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index cf26ec1950..2e08e66a4a 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -9,6 +9,7 @@ import { tint, useTheme } from "@tui/context/theme" import { EmptyBorder, SplitBorder } from "@tui/component/border" import { useSDK } from "@tui/context/sdk" import { useRoute } from "@tui/context/route" +import { useProject } from "@tui/context/project" import { useSync } from "@tui/context/sync" import { useEvent } from "@tui/context/event" import { MessageID, PartID } from "@/session/schema" @@ -38,6 +39,8 @@ import { useKV } from "../../context/kv" import { createFadeIn } from "../../util/signal" import { useTextareaKeybindings } from "../textarea-keybindings" import { DialogSkill } from "../dialog-skill" +import { DialogWorkspaceCreate, restoreWorkspaceSession } from "../dialog-workspace-create" +import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable" import { useArgs } from "@tui/context/args" export type PromptProps = { @@ -92,6 +95,7 @@ export function Prompt(props: PromptProps) { const args = useArgs() const sdk = useSDK() const route = useRoute() + const project = useProject() const sync = useSync() const dialog = useDialog() const toast = useToast() @@ -241,9 +245,11 @@ export function Prompt(props: PromptProps) { keybind: "input_submit", category: "Prompt", hidden: true, - onSelect: (dialog) => { + onSelect: async (dialog) => { if (!input.focused) return - void submit() + const handled = await submit() + if (!handled) return + dialog.clear() }, }, @@ -628,20 +634,48 @@ export function Prompt(props: PromptProps) { setStore("prompt", "input", input.plainText) syncExtmarksWithPromptParts() } - if (props.disabled) return - if (autocomplete?.visible) return - if (!store.prompt.input) return + if (props.disabled) return false + if (autocomplete?.visible) return false + if (!store.prompt.input) return false const agent = local.agent.current() - if (!agent) return + if (!agent) return false const trimmed = store.prompt.input.trim() if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") { void exit() - return + return true } const selectedModel = local.model.current() if (!selectedModel) { void promptModelWarning() - return + return false + } + + const workspaceSession = props.sessionID ? sync.session.get(props.sessionID) : undefined + const workspaceID = workspaceSession?.workspaceID + const workspaceStatus = workspaceID ? (project.workspace.status(workspaceID) ?? "error") : undefined + if (props.sessionID && workspaceID && workspaceStatus !== "connected") { + dialog.replace(() => ( + { + dialog.replace(() => ( + + restoreWorkspaceSession({ + dialog, + sdk, + sync, + project, + toast, + workspaceID: nextWorkspaceID, + sessionID: props.sessionID!, + }) + } + /> + )) + }} + /> + )) + return false } let sessionID = props.sessionID @@ -656,7 +690,7 @@ export function Prompt(props: PromptProps) { variant: "error", }) - return + return true } sessionID = res.data.id @@ -770,6 +804,7 @@ export function Prompt(props: PromptProps) { }) }, 50) input.clear() + return true } const exit = useExit() diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index 06bc270644..4a7b711a03 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -1,3 +1,4 @@ +import { useProject } from "@tui/context/project" import { useSync } from "@tui/context/sync" import { createMemo, Show } from "solid-js" import { useTheme } from "../../context/theme" @@ -8,10 +9,23 @@ import { TuiPluginRuntime } from "../../plugin" import { getScrollAcceleration } from "../../util/scroll" export function Sidebar(props: { sessionID: string; overlay?: boolean }) { + const project = useProject() const sync = useSync() const { theme } = useTheme() const tuiConfig = useTuiConfig() const session = createMemo(() => sync.session.get(props.sessionID)) + const workspaceStatus = () => { + const workspaceID = session()?.workspaceID + if (!workspaceID) return "error" + return project.workspace.status(workspaceID) ?? "error" + } + const workspaceLabel = () => { + const workspaceID = session()?.workspaceID + if (!workspaceID) return "unknown" + const info = project.workspace.get(workspaceID) + if (!info) return "unknown" + return `${info.type}: ${info.name}` + } const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig)) return ( @@ -48,6 +62,12 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { {session()!.title} + + + {" "} + {workspaceLabel()} + + {session()!.share!.url} From d6e1362fee569e9d9fa9bc0c6c982f64b2027b30 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 19:15:07 +0000 Subject: [PATCH 116/335] chore: generate --- .../src/cli/cmd/tui/component/dialog-workspace-create.tsx | 5 +---- .../cli/cmd/tui/component/dialog-workspace-unavailable.tsx | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index 6dcdabe0b9..a16c98a9f4 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -145,10 +145,7 @@ export async function restoreWorkspaceSession(input: { await input.sync.bootstrap({ fatal: false }) } catch (e) {} - await Promise.all([ - input.project.workspace.sync(), - input.sync.session.sync(input.sessionID), - ]).catch((err) => { + await Promise.all([input.project.workspace.sync(), input.sync.session.sync(input.sessionID)]).catch((err) => { log.error("session restore refresh failed", { workspaceID: input.workspaceID, sessionID: input.sessionID, diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx index 0c2dd3e2f3..7a21798534 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx @@ -5,9 +5,7 @@ import { For } from "solid-js" import { useTheme } from "../context/theme" import { useDialog } from "../ui/dialog" -export function DialogWorkspaceUnavailable(props: { - onRestore?: () => boolean | void | Promise -}) { +export function DialogWorkspaceUnavailable(props: { onRestore?: () => boolean | void | Promise }) { const dialog = useDialog() const { theme } = useTheme() const [store, setStore] = createStore({ From 88582566bf2bfd2d26000f0c25735bf48ddeca00 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 19:18:55 +0000 Subject: [PATCH 117/335] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 5fe1189579..01366c82dc 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-60KBy/mySuIKbBD5aCS4ZAQqnwZ4PjLMAqZH7gpKCFY=", - "aarch64-linux": "sha256-ROd9OfdCBQZntoAr32O3YVl7ljRgYvJma25U+jHwcts=", - "aarch64-darwin": "sha256-MjMfR73ZZLXtIXfuzqpjvD5RxmIRi9HA1jWXPvagU6w=", - "x86_64-darwin": "sha256-1BADHsSdMxJUbQ4DR/Ww4ZTt18H365lETJs7Fy7fsLc=" + "x86_64-linux": "sha256-GjpBQhvGLTM6NWX29b/mS+KjrQPl0w9VjQHH5jaK9SM=", + "aarch64-linux": "sha256-F5h9p+iZ8CASdUYaYR7O22NwBRa/iT+ZinUxO8lbPTc=", + "aarch64-darwin": "sha256-jWo5yvCtjVKRf9i5XUcTTaLtj2+G6+T1Td2llO/cT5I=", + "x86_64-darwin": "sha256-LzV+5/8P2mkiFHmt+a8zDeJjRbU8z9nssSA4tzv1HxA=" } } From 5621373bc2d5ca106e9fbb2d2fbc3bc547dd744c Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 17 Apr 2026 15:20:11 -0400 Subject: [PATCH 118/335] fix(core): move instance middleware after control plane routes (#23150) --- .../src/server/routes/instance/index.ts | 6 ++- .../src/server/routes/instance/middleware.ts | 35 +++++++++++++++ packages/opencode/src/server/server.ts | 44 +++---------------- 3 files changed, 46 insertions(+), 39 deletions(-) create mode 100644 packages/opencode/src/server/routes/instance/middleware.ts diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index 017541b8fc..c0339fded7 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -15,6 +15,7 @@ import { Command } from "@/command" import { QuestionRoutes } from "./question" import { PermissionRoutes } from "./permission" import { Flag } from "@/flag/flag" +import { WorkspaceID } from "@/control-plane/schema" import { ExperimentalHttpApiServer } from "./httpapi/server" import { ProjectRoutes } from "./project" import { SessionRoutes } from "./session" @@ -27,9 +28,10 @@ import { ProviderRoutes } from "./provider" import { EventRoutes } from "./event" import { SyncRoutes } from "./sync" import { AppRuntime } from "@/effect/app-runtime" +import { InstanceMiddleware } from "./middleware" -export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { - const app = new Hono() +export const InstanceRoutes = (upgrade: UpgradeWebSocket, workspaceID?: WorkspaceID): Hono => { + const app = new Hono().use(InstanceMiddleware(workspaceID)) if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) { const handler = ExperimentalHttpApiServer.webHandler().handler diff --git a/packages/opencode/src/server/routes/instance/middleware.ts b/packages/opencode/src/server/routes/instance/middleware.ts new file mode 100644 index 0000000000..b963268d64 --- /dev/null +++ b/packages/opencode/src/server/routes/instance/middleware.ts @@ -0,0 +1,35 @@ +import type { MiddlewareHandler } from "hono" +import { Instance } from "@/project/instance" +import { InstanceBootstrap } from "@/project/bootstrap" +import { AppRuntime } from "@/effect/app-runtime" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { WorkspaceContext } from "@/control-plane/workspace-context" +import { WorkspaceID } from "@/control-plane/schema" + +export function InstanceMiddleware(workspaceID?: WorkspaceID): MiddlewareHandler { + return async (c, next) => { + const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() + const directory = AppFileSystem.resolve( + (() => { + try { + return decodeURIComponent(raw) + } catch { + return raw + } + })(), + ) + + return WorkspaceContext.provide({ + workspaceID, + async fn() { + return Instance.provide({ + directory, + init: () => AppRuntime.runPromise(InstanceBootstrap), + async fn() { + return next() + }, + }) + }, + }) + } +} diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 2201c75b4c..f608c2b732 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -1,16 +1,10 @@ import { generateSpecs } from "hono-openapi" import { Hono } from "hono" -import type { MiddlewareHandler } from "hono" import { adapter } from "#hono" import { lazy } from "@/util/lazy" import { Log } from "@/util" import { Flag } from "@/flag/flag" -import { Instance } from "@/project/instance" -import { InstanceBootstrap } from "@/project/bootstrap" -import { AppRuntime } from "@/effect/app-runtime" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { WorkspaceID } from "@/control-plane/schema" -import { WorkspaceContext } from "@/control-plane/workspace-context" import { MDNS } from "./mdns" import { AuthMiddleware, CompressionMiddleware, CorsMiddleware, ErrorMiddleware, LoggerMiddleware } from "./middleware" import { FenceMiddleware } from "./fence" @@ -48,47 +42,23 @@ function create(opts: { cors?: string[] }) { const runtime = adapter.create(app) - function InstanceMiddleware(workspaceID?: WorkspaceID): MiddlewareHandler { - return async (c, next) => { - const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() - const directory = AppFileSystem.resolve( - (() => { - try { - return decodeURIComponent(raw) - } catch { - return raw - } - })(), - ) - - return WorkspaceContext.provide({ - workspaceID, - async fn() { - return Instance.provide({ - directory, - init: () => AppRuntime.runPromise(InstanceBootstrap), - async fn() { - return next() - }, - }) - }, - }) - } - } - if (Flag.OPENCODE_WORKSPACE_ID) { return { app: app - .use(InstanceMiddleware(Flag.OPENCODE_WORKSPACE_ID ? WorkspaceID.make(Flag.OPENCODE_WORKSPACE_ID) : undefined)) .use(FenceMiddleware) - .route("/", InstanceRoutes(runtime.upgradeWebSocket)), + .route( + "/", + InstanceRoutes( + runtime.upgradeWebSocket, + Flag.OPENCODE_WORKSPACE_ID ? WorkspaceID.make(Flag.OPENCODE_WORKSPACE_ID) : undefined, + ), + ), runtime, } } return { app: app - .use(InstanceMiddleware()) .route("/", ControlPlaneRoutes()) .use(WorkspaceRouterMiddleware(runtime.upgradeWebSocket)) .route("/", InstanceRoutes(runtime.upgradeWebSocket)) From 68834cfcc37ce51c1b0b4895b9563a86f1611c9a Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 15:22:08 -0400 Subject: [PATCH 119/335] fix(opencode): normalize provider metadata and tag otel runs (#23140) --- packages/opencode/src/cli/cmd/tui/thread.ts | 9 ++-- packages/opencode/src/cli/cmd/tui/worker.ts | 3 ++ packages/opencode/src/effect/observability.ts | 50 +++++++++++-------- packages/opencode/src/file/ripgrep.ts | 5 +- packages/opencode/src/file/ripgrep.worker.ts | 5 +- packages/opencode/src/index.ts | 5 ++ packages/opencode/src/provider/provider.ts | 14 +++--- .../opencode/src/util/opencode-process.ts | 24 +++++++++ .../opencode/test/provider/provider.test.ts | 34 ++++++++++++- 9 files changed, 112 insertions(+), 37 deletions(-) create mode 100644 packages/opencode/src/util/opencode-process.ts diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 96ceb905c5..e3e9eb8117 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -15,6 +15,7 @@ import type { EventSource } from "./context/sdk" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" import { writeHeapSnapshot } from "v8" import { TuiConfig } from "./config/tui" +import { OPENCODE_PROCESS_ROLE, OPENCODE_RUN_ID, ensureRunID, sanitizedProcessEnv } from "@/util/opencode-process" declare global { const OPENCODE_WORKER_PATH: string @@ -129,11 +130,13 @@ export const TuiThreadCommand = cmd({ return } const cwd = Filesystem.resolve(process.cwd()) + const env = sanitizedProcessEnv({ + [OPENCODE_PROCESS_ROLE]: "worker", + [OPENCODE_RUN_ID]: ensureRunID(), + }) const worker = new Worker(file, { - env: Object.fromEntries( - Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined), - ), + env, }) worker.onerror = (e) => { Log.Default.error("thread error", { diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index 393a407eb0..8cec99c615 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -11,6 +11,9 @@ import { Flag } from "@/flag/flag" import { writeHeapSnapshot } from "node:v8" import { Heap } from "@/cli/heap" import { AppRuntime } from "@/effect/app-runtime" +import { ensureProcessMetadata } from "@/util/opencode-process" + +ensureProcessMetadata("worker") await Log.init({ print: process.argv.includes("--print-logs"), diff --git a/packages/opencode/src/effect/observability.ts b/packages/opencode/src/effect/observability.ts index efd16ffc09..1c385d60ae 100644 --- a/packages/opencode/src/effect/observability.ts +++ b/packages/opencode/src/effect/observability.ts @@ -4,9 +4,11 @@ import { OtlpLogger, OtlpSerialization } from "effect/unstable/observability" import * as EffectLogger from "./logger" import { Flag } from "@/flag/flag" import { InstallationChannel, InstallationVersion } from "@/installation/version" +import { ensureProcessMetadata } from "@/util/opencode-process" const base = Flag.OTEL_EXPORTER_OTLP_ENDPOINT export const enabled = !!base +const processID = crypto.randomUUID() const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS ? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce( @@ -19,26 +21,34 @@ const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS ) : undefined -const resource = { - serviceName: "opencode", - serviceVersion: InstallationVersion, - attributes: { - "deployment.environment.name": InstallationChannel, - "opencode.client": Flag.OPENCODE_CLIENT, - }, +function resource() { + const processMetadata = ensureProcessMetadata("main") + return { + serviceName: "opencode", + serviceVersion: InstallationVersion, + attributes: { + "deployment.environment.name": InstallationChannel, + "opencode.client": Flag.OPENCODE_CLIENT, + "opencode.process_role": processMetadata.processRole, + "opencode.run_id": processMetadata.runID, + "service.instance.id": processID, + }, + } } -const logs = Logger.layer( - [ - EffectLogger.logger, - OtlpLogger.make({ - url: `${base}/v1/logs`, - resource, - headers, - }), - ], - { mergeWithExisting: false }, -).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer)) +function logs() { + return Logger.layer( + [ + EffectLogger.logger, + OtlpLogger.make({ + url: `${base}/v1/logs`, + resource: resource(), + headers, + }), + ], + { mergeWithExisting: false }, + ).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer)) +} const traces = async () => { const NodeSdk = await import("@effect/opentelemetry/NodeSdk") @@ -58,7 +68,7 @@ const traces = async () => { context.setGlobalContextManager(mgr) return NodeSdk.layer(() => ({ - resource, + resource: resource(), spanProcessor: new SdkBase.BatchSpanProcessor( new OTLP.OTLPTraceExporter({ url: `${base}/v1/traces`, @@ -73,7 +83,7 @@ export const layer = !base : Layer.unwrap( Effect.gen(function* () { const trace = yield* Effect.promise(traces) - return Layer.mergeAll(trace, logs) + return Layer.mergeAll(trace, logs()) }), ) diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index ac450108e1..3f16f6c501 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -7,6 +7,7 @@ import { ripgrep } from "ripgrep" import { Filesystem } from "@/util" import { Log } from "@/util" +import { sanitizedProcessEnv } from "@/util/opencode-process" const log = Log.create({ service: "ripgrep" }) @@ -157,9 +158,7 @@ type WorkerError = { } function env() { - const env = Object.fromEntries( - Object.entries(process.env).filter((item): item is [string, string] => item[1] !== undefined), - ) + const env = sanitizedProcessEnv() delete env.RIPGREP_CONFIG_PATH return env } diff --git a/packages/opencode/src/file/ripgrep.worker.ts b/packages/opencode/src/file/ripgrep.worker.ts index 62094c7acc..21a3aef5cc 100644 --- a/packages/opencode/src/file/ripgrep.worker.ts +++ b/packages/opencode/src/file/ripgrep.worker.ts @@ -1,9 +1,8 @@ import { ripgrep } from "ripgrep" +import { sanitizedProcessEnv } from "@/util/opencode-process" function env() { - const env = Object.fromEntries( - Object.entries(process.env).filter((item): item is [string, string] => item[1] !== undefined), - ) + const env = sanitizedProcessEnv() delete env.RIPGREP_CONFIG_PATH return env } diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 67de87c2aa..0a3a927b46 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -38,6 +38,9 @@ import { errorMessage } from "./util/error" import { PluginCommand } from "./cli/cmd/plug" import { Heap } from "./cli/heap" import { drizzle } from "drizzle-orm/bun-sqlite" +import { ensureProcessMetadata } from "./util/opencode-process" + +const processMetadata = ensureProcessMetadata("main") process.on("unhandledRejection", (e) => { Log.Default.error("rejection", { @@ -108,6 +111,8 @@ const cli = yargs(args) Log.Default.info("opencode", { version: InstallationVersion, args: process.argv.slice(2), + process_role: processMetadata.processRole, + run_id: processMetadata.runID, }) const marker = path.join(Global.Path.data, "opencode.db") diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 558bcb75a5..51aca21c77 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -968,7 +968,7 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model family: model.family, api: { id: model.id, - url: model.provider?.api ?? provider.api!, + url: model.provider?.api ?? provider.api ?? "", npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible", }, status: model.status ?? "active", @@ -981,10 +981,10 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model output: model.limit.output, }, capabilities: { - temperature: model.temperature, - reasoning: model.reasoning, - attachment: model.attachment, - toolcall: model.tool_call, + temperature: model.temperature ?? false, + reasoning: model.reasoning ?? false, + attachment: model.attachment ?? false, + toolcall: model.tool_call ?? true, input: { text: model.modalities?.input?.includes("text") ?? false, audio: model.modalities?.input?.includes("audio") ?? false, @@ -1001,7 +1001,7 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model }, interleaved: model.interleaved ?? false, }, - release_date: model.release_date, + release_date: model.release_date ?? "", variants: {}, } @@ -1143,7 +1143,7 @@ const layer: Layer.Layer< existingModel?.api.npm ?? modelsDev[providerID]?.npm ?? "@ai-sdk/openai-compatible", - url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api, + url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api ?? "", }, status: model.status ?? existingModel?.status ?? "active", name, diff --git a/packages/opencode/src/util/opencode-process.ts b/packages/opencode/src/util/opencode-process.ts new file mode 100644 index 0000000000..f59270ad2d --- /dev/null +++ b/packages/opencode/src/util/opencode-process.ts @@ -0,0 +1,24 @@ +export const OPENCODE_RUN_ID = "OPENCODE_RUN_ID" +export const OPENCODE_PROCESS_ROLE = "OPENCODE_PROCESS_ROLE" + +export function ensureRunID() { + return (process.env[OPENCODE_RUN_ID] ??= crypto.randomUUID()) +} + +export function ensureProcessRole(fallback: "main" | "worker") { + return (process.env[OPENCODE_PROCESS_ROLE] ??= fallback) +} + +export function ensureProcessMetadata(fallback: "main" | "worker") { + return { + runID: ensureRunID(), + processRole: ensureProcessRole(fallback), + } +} + +export function sanitizedProcessEnv(overrides?: Record) { + const env = Object.fromEntries( + Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined), + ) + return overrides ? Object.assign(env, overrides) : env +} diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index df8fc4e966..8993020820 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1916,7 +1916,7 @@ test("mode cost preserves over-200k pricing from base model", () => { }, }, }, - } as ModelsDev.Provider + } as unknown as ModelsDev.Provider const model = Provider.fromModelsDevProvider(provider).models["gpt-5.4-fast"] expect(model.cost.input).toEqual(5) @@ -1934,6 +1934,38 @@ test("mode cost preserves over-200k pricing from base model", () => { }) }) +test("models.dev normalization fills required response fields", () => { + const provider = { + id: "gateway", + name: "Gateway", + env: [], + models: { + "gpt-5.4": { + id: "gpt-5.4", + name: "GPT-5.4", + family: "gpt", + cost: { + input: 2.5, + output: 15, + }, + limit: { + context: 1_050_000, + input: 922_000, + output: 128_000, + }, + }, + }, + } as unknown as ModelsDev.Provider + + const model = Provider.fromModelsDevProvider(provider).models["gpt-5.4"] + expect(model.api.url).toBe("") + expect(model.capabilities.temperature).toBe(false) + expect(model.capabilities.reasoning).toBe(false) + expect(model.capabilities.attachment).toBe(false) + expect(model.capabilities.toolcall).toBe(true) + expect(model.release_date).toBe("") +}) + test("model variants are generated for reasoning models", async () => { await using tmp = await tmpdir({ init: async (dir) => { From aa05b9abe5ff3a4b29b72fb878be969567eda5bb Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 17 Apr 2026 15:25:58 -0400 Subject: [PATCH 120/335] fix(core): pass OTEL config to workspace env (#23154) --- packages/opencode/src/control-plane/types.ts | 2 +- packages/opencode/src/control-plane/workspace.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/control-plane/types.ts b/packages/opencode/src/control-plane/types.ts index 3961cd0e2a..07acd5ce58 100644 --- a/packages/opencode/src/control-plane/types.ts +++ b/packages/opencode/src/control-plane/types.ts @@ -28,7 +28,7 @@ export type WorkspaceAdaptor = { name: string description: string configure(info: WorkspaceInfo): WorkspaceInfo | Promise - create(info: WorkspaceInfo, env: Record, from?: WorkspaceInfo): Promise + create(info: WorkspaceInfo, env: Record, from?: WorkspaceInfo): Promise remove(info: WorkspaceInfo): Promise target(info: WorkspaceInfo): Target | Promise } diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index d678ad7526..e94d6c2c93 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -115,6 +115,8 @@ export const create = fn(CreateInput, async (input) => { OPENCODE_AUTH_CONTENT: JSON.stringify(await AppRuntime.runPromise(Auth.Service.use((auth) => auth.all()))), OPENCODE_WORKSPACE_ID: config.id, OPENCODE_EXPERIMENTAL_WORKSPACES: "true", + OTEL_EXPORTER_OTLP_HEADERS: process.env.OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, } await adaptor.create(config, env) From f83cecaaf6ee29da70109575595901884cdbc312 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 15:29:32 -0400 Subject: [PATCH 121/335] fix(opencode): untrace streaming event hot paths (#23156) --- packages/opencode/src/session/processor.ts | 2 +- packages/opencode/src/session/session.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 820c61aa91..900579f036 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -213,7 +213,7 @@ export const layer: Layer.Layer< return true }) - const handleEvent = Effect.fn("SessionProcessor.handleEvent")(function* (value: StreamEvent) { + const handleEvent = Effect.fnUntraced(function* (value: StreamEvent) { switch (value.type) { case "start": yield* status.set(ctx.sessionID, { type: "busy" }) diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 077cc43097..1941a6704e 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -649,10 +649,10 @@ export const layer: Layer.Layer = return input.partID }) - const updatePartDelta = Effect.fn("Session.updatePartDelta")(function* (input: { - sessionID: SessionID - messageID: MessageID - partID: PartID + const updatePartDelta = Effect.fnUntraced(function* (input: { + sessionID: SessionID + messageID: MessageID + partID: PartID field: string delta: string }) { From 9b0659d4f9b64cb2416bb340c503037d0778349e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 19:30:28 +0000 Subject: [PATCH 122/335] chore: generate --- packages/opencode/src/session/processor.ts | 2 +- packages/opencode/src/session/session.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 900579f036..9ab74ca341 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -213,7 +213,7 @@ export const layer: Layer.Layer< return true }) - const handleEvent = Effect.fnUntraced(function* (value: StreamEvent) { + const handleEvent = Effect.fnUntraced(function* (value: StreamEvent) { switch (value.type) { case "start": yield* status.set(ctx.sessionID, { type: "busy" }) diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 1941a6704e..5168b80b56 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -649,10 +649,10 @@ export const layer: Layer.Layer = return input.partID }) - const updatePartDelta = Effect.fnUntraced(function* (input: { - sessionID: SessionID - messageID: MessageID - partID: PartID + const updatePartDelta = Effect.fnUntraced(function* (input: { + sessionID: SessionID + messageID: MessageID + partID: PartID field: string delta: string }) { From 9b6c3971712baaecacb1d09eb21c4d6f77955622 Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 17 Apr 2026 20:13:25 +0000 Subject: [PATCH 123/335] release: v1.4.10 --- bun.lock | 32 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop-electron/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++----- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/shared/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 19 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 8d83d8ab47..6e8845c4f2 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.4.9", + "version": "1.4.10", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -83,7 +83,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.4.9", + "version": "1.4.10", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -117,7 +117,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.4.9", + "version": "1.4.10", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -144,7 +144,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.4.9", + "version": "1.4.10", "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/openai": "3.0.48", @@ -168,7 +168,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.4.9", + "version": "1.4.10", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -192,7 +192,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.4.9", + "version": "1.4.10", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -225,7 +225,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "1.4.9", + "version": "1.4.10", "dependencies": { "effect": "catalog:", "electron-context-menu": "4.1.2", @@ -268,7 +268,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.4.9", + "version": "1.4.10", "dependencies": { "@opencode-ai/shared": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -297,7 +297,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.4.9", + "version": "1.4.10", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -313,7 +313,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.4.9", + "version": "1.4.10", "bin": { "opencode": "./bin/opencode", }, @@ -458,7 +458,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.4.9", + "version": "1.4.10", "dependencies": { "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", @@ -493,7 +493,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.4.9", + "version": "1.4.10", "dependencies": { "cross-spawn": "catalog:", }, @@ -508,7 +508,7 @@ }, "packages/shared": { "name": "@opencode-ai/shared", - "version": "1.4.9", + "version": "1.4.10", "bin": { "opencode": "./bin/opencode", }, @@ -532,7 +532,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.4.9", + "version": "1.4.10", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -567,7 +567,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.4.9", + "version": "1.4.10", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -616,7 +616,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.4.9", + "version": "1.4.10", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 0f4ae4228b..7206bd5394 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.4.9", + "version": "1.4.10", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 63b6a5c414..2dbd630163 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.4.9", + "version": "1.4.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index e5351c1a87..323ee013df 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.4.9", + "version": "1.4.10", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index ef84eae470..bef6f53e08 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.4.9", + "version": "1.4.10", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 439beb15c8..5b56f5678d 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.4.9", + "version": "1.4.10", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index b698a08896..dbf63e8500 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "1.4.9", + "version": "1.4.10", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 1d9d24bd32..97c8713a2b 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.4.9", + "version": "1.4.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 02f1ad83ca..914978d7da 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.4.9", + "version": "1.4.10", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index ffcc975d1b..55b3673739 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.4.9" +version = "1.4.10" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.9/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.9/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.9/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.9/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.9/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 2ac49a9228..1dc1f32bb1 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.4.9", + "version": "1.4.10", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 2f56b74775..7d24d7ca31 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.4.9", + "version": "1.4.10", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index a1aa6470dc..cc1f24b68e 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.4.9", + "version": "1.4.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 27d9188151..3a6d13680e 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.4.9", + "version": "1.4.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/shared/package.json b/packages/shared/package.json index 383b26d6ed..615cd42c00 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.4.9", + "version": "1.4.10", "name": "@opencode-ai/shared", "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index 75974ed39e..eb23c2036f 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.4.9", + "version": "1.4.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 525cf20935..e756c33fca 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.4.9", + "version": "1.4.10", "type": "module", "license": "MIT", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index d3a2a25e4c..0c4a0c6480 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.4.9", + "version": "1.4.10", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index a3ed76c883..83d00c987a 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.4.9", + "version": "1.4.10", "publisher": "sst-dev", "repository": { "type": "git", From b708e8431ec3fcda2a53a3c3e3b8608e5edfb16d Mon Sep 17 00:00:00 2001 From: Dax Date: Fri, 17 Apr 2026 15:56:52 -0400 Subject: [PATCH 124/335] docs(opencode): annotate plugin loader flow (#23160) --- packages/opencode/src/plugin/loader.ts | 46 ++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/plugin/loader.ts b/packages/opencode/src/plugin/loader.ts index 0245d311e0..f17673f36e 100644 --- a/packages/opencode/src/plugin/loader.ts +++ b/packages/opencode/src/plugin/loader.ts @@ -12,31 +12,41 @@ import { ConfigPlugin } from "@/config/plugin" import { InstallationVersion } from "@/installation/version" export namespace PluginLoader { + // A normalized plugin declaration derived from config before any filesystem or npm work happens. export type Plan = { spec: string options: ConfigPlugin.Options | undefined deprecated: boolean } + + // A plugin that has been resolved to a concrete target and entrypoint on disk. export type Resolved = Plan & { source: PluginSource target: string entry: string pkg?: PluginPackage } + + // A plugin target we could inspect, but which does not expose the requested kind of entrypoint. export type Missing = Plan & { source: PluginSource target: string pkg?: PluginPackage message: string } + + // A resolved plugin whose module has been imported successfully. export type Loaded = Resolved & { mod: Record } type Candidate = { origin: ConfigPlugin.Origin; plan: Plan } type Report = { + // Called before each attempt so callers can log initial load attempts and retries uniformly. start?: (candidate: Candidate, retry: boolean) => void + // Called when the package exists but does not provide the requested entrypoint. missing?: (candidate: Candidate, retry: boolean, message: string, resolved: Missing) => void + // Called for operational failures such as install, compatibility, or dynamic import errors. error?: ( candidate: Candidate, retry: boolean, @@ -46,19 +56,25 @@ export namespace PluginLoader { ) => void } + // Normalize a config item into the loader's internal representation. function plan(item: ConfigPlugin.Spec): Plan { const spec = ConfigPlugin.pluginSpecifier(item) return { spec, options: ConfigPlugin.pluginOptions(item), deprecated: isDeprecatedPlugin(spec) } } + // Resolve a configured plugin into a concrete entrypoint that can later be imported. + // + // The stages here intentionally separate install/target resolution, entrypoint detection, + // and compatibility checks so callers can report the exact reason a plugin was skipped. export async function resolve( plan: Plan, kind: PluginKind, ): Promise< | { ok: true; value: Resolved } - | { ok: false; stage: "missing"; value: Missing } - | { ok: false; stage: "install" | "entry" | "compatibility"; error: unknown } + | { ok: false; stage: "missing"; value: Missing } + | { ok: false; stage: "install" | "entry" | "compatibility"; error: unknown } > { + // First make sure the plugin exists locally, installing npm plugins on demand. let target = "" try { target = await resolvePluginTarget(plan.spec) @@ -67,6 +83,7 @@ export namespace PluginLoader { } if (!target) return { ok: false, stage: "install", error: new Error(`Plugin ${plan.spec} target is empty`) } + // Then inspect the target for the requested server/tui entrypoint. let base try { base = await createPluginEntry(plan.spec, target, kind) @@ -86,6 +103,8 @@ export namespace PluginLoader { }, } + // npm plugins can declare which opencode versions they support; file plugins are treated + // as local development code and skip this compatibility gate. if (base.source === "npm") { try { await checkPluginCompatibility(base.target, InstallationVersion, base.pkg) @@ -96,6 +115,7 @@ export namespace PluginLoader { return { ok: true, value: { ...plan, source: base.source, target: base.target, entry: base.entry, pkg: base.pkg } } } + // Import the resolved module only after all earlier validation has succeeded. export async function load(row: Resolved): Promise<{ ok: true; value: Loaded } | { ok: false; error: unknown }> { let mod try { @@ -107,6 +127,8 @@ export namespace PluginLoader { return { ok: true, value: { ...row, mod } } } + // Run one candidate through the full pipeline: resolve, optionally surface a missing entry, + // import the module, and finally let the caller transform the loaded plugin into any result type. async function attempt( candidate: Candidate, kind: PluginKind, @@ -116,11 +138,17 @@ export namespace PluginLoader { report: Report | undefined, ): Promise { const plan = candidate.plan + + // Deprecated plugin packages are silently ignored because they are now built in. if (plan.deprecated) return + report?.start?.(candidate, retry) + const resolved = await resolve(plan, kind) if (!resolved.ok) { if (resolved.stage === "missing") { + // Missing entrypoints are handled separately so callers can still inspect package metadata, + // for example to load theme files from a tui plugin package that has no code entrypoint. if (missing) { const value = await missing(resolved.value, candidate.origin, retry) if (value !== undefined) return value @@ -131,11 +159,15 @@ export namespace PluginLoader { report?.error?.(candidate, retry, resolved.stage, resolved.error) return } + const loaded = await load(resolved.value) if (!loaded.ok) { report?.error?.(candidate, retry, "load", loaded.error, resolved.value) return } + + // The default behavior is to return the successfully loaded plugin as-is, but callers can + // provide a finisher to adapt the result into a more specific runtime shape. if (!finish) return loaded.value as R return finish(loaded.value, candidate.origin, retry) } @@ -149,6 +181,11 @@ export namespace PluginLoader { report?: Report } + // Resolve and load all configured plugins in parallel. + // + // If `wait` is provided, file-based plugins that initially failed are retried once after the + // caller finishes preparing dependencies. This supports local plugins that depend on an install + // step happening elsewhere before their entrypoint becomes loadable. export async function loadExternal(input: Input): Promise { const candidates = input.items.map((origin) => ({ origin, plan: plan(origin.spec) })) const list: Array> = [] @@ -160,6 +197,9 @@ export namespace PluginLoader { let deps: Promise | undefined for (let i = 0; i < candidates.length; i++) { if (out[i] !== undefined) continue + + // Only local file plugins are retried. npm plugins already attempted installation during + // the first pass, while file plugins may need the caller's dependency preparation to finish. const candidate = candidates[i] if (!candidate || pluginSource(candidate.plan.spec) !== "file") continue deps ??= input.wait() @@ -167,6 +207,8 @@ export namespace PluginLoader { out[i] = await attempt(candidate, input.kind, true, input.finish, input.missing, input.report) } } + + // Drop skipped/failed entries while preserving the successful result order. const ready: R[] = [] for (const item of out) if (item !== undefined) ready.push(item) return ready From c2061c6bbfa83147792ab9aec251cf193ccb6b0d Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 19:58:18 +0000 Subject: [PATCH 125/335] chore: generate --- packages/opencode/src/plugin/loader.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/plugin/loader.ts b/packages/opencode/src/plugin/loader.ts index f17673f36e..e61612561b 100644 --- a/packages/opencode/src/plugin/loader.ts +++ b/packages/opencode/src/plugin/loader.ts @@ -71,8 +71,8 @@ export namespace PluginLoader { kind: PluginKind, ): Promise< | { ok: true; value: Resolved } - | { ok: false; stage: "missing"; value: Missing } - | { ok: false; stage: "install" | "entry" | "compatibility"; error: unknown } + | { ok: false; stage: "missing"; value: Missing } + | { ok: false; stage: "install" | "entry" | "compatibility"; error: unknown } > { // First make sure the plugin exists locally, installing npm plugins on demand. let target = "" From 984f5ed6eb0cbf3048d8c1dbd8f643f5a01d2501 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 16:15:24 -0400 Subject: [PATCH 126/335] fix(opencode): skip share sync for unshared sessions (#23159) --- packages/opencode/src/share/share-next.ts | 41 ++++++++++++++++++----- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index 3484d5da76..2622f4f7f0 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -38,8 +38,9 @@ const ShareSchema = Schema.Struct({ export type Share = typeof ShareSchema.Type type State = { - queue: Map }> + queue: Map> scope: Scope.Closeable + shared: Map } type Data = @@ -118,17 +119,20 @@ export const layer = Layer.effect( function sync(sessionID: SessionID, data: Data[]): Effect.Effect { return Effect.gen(function* () { if (disabled) return + const share = yield* getCached(sessionID) + if (!share) return + const s = yield* InstanceState.get(state) const existing = s.queue.get(sessionID) if (existing) { for (const item of data) { - existing.data.set(key(item), item) + existing.set(key(item), item) } return } const next = new Map(data.map((item) => [key(item), item])) - s.queue.set(sessionID, { data: next }) + s.queue.set(sessionID, next) yield* flush(sessionID).pipe( Effect.delay(1000), Effect.catchCause((cause) => @@ -143,13 +147,14 @@ export const layer = Layer.effect( const state: InstanceState.InstanceState = yield* InstanceState.make( Effect.fn("ShareNext.state")(function* (_ctx) { - const cache: State = { queue: new Map(), scope: yield* Scope.make() } + const cache: State = { queue: new Map(), scope: yield* Scope.make(), shared: new Map() } yield* Effect.addFinalizer(() => Scope.close(cache.scope, Exit.void).pipe( Effect.andThen( Effect.sync(() => { cache.queue.clear() + cache.shared.clear() }), ), ), @@ -227,6 +232,18 @@ export const layer = Layer.effect( return { id: row.id, secret: row.secret, url: row.url } satisfies Share }) + const getCached = Effect.fnUntraced(function* (sessionID: SessionID) { + const s = yield* InstanceState.get(state) + if (s.shared.has(sessionID)) { + const cached = s.shared.get(sessionID) + return cached === null ? undefined : cached + } + + const share = yield* get(sessionID) + s.shared.set(sessionID, share ?? null) + return share + }) + const flush = Effect.fn("ShareNext.flush")(function* (sessionID: SessionID) { if (disabled) return const s = yield* InstanceState.get(state) @@ -235,13 +252,13 @@ export const layer = Layer.effect( s.queue.delete(sessionID) - const share = yield* get(sessionID) + const share = yield* getCached(sessionID) if (!share) return const req = yield* request() const res = yield* HttpClientRequest.post(`${req.baseUrl}${req.api.sync(share.id)}`).pipe( HttpClientRequest.setHeaders(req.headers), - HttpClientRequest.bodyJson({ secret: share.secret, data: Array.from(queued.data.values()) }), + HttpClientRequest.bodyJson({ secret: share.secret, data: Array.from(queued.values()) }), Effect.flatMap((r) => http.execute(r)), ) @@ -307,6 +324,7 @@ export const layer = Layer.effect( .run(), ) const s = yield* InstanceState.get(state) + s.shared.set(sessionID, result) yield* full(sessionID).pipe( Effect.catchCause((cause) => Effect.sync(() => { @@ -321,8 +339,13 @@ export const layer = Layer.effect( const remove = Effect.fn("ShareNext.remove")(function* (sessionID: SessionID) { if (disabled) return log.info("removing share", { sessionID }) - const share = yield* get(sessionID) - if (!share) return + const s = yield* InstanceState.get(state) + const share = yield* getCached(sessionID) + if (!share) { + s.shared.delete(sessionID) + s.queue.delete(sessionID) + return + } const req = yield* request() yield* HttpClientRequest.delete(`${req.baseUrl}${req.api.remove(share.id)}`).pipe( @@ -332,6 +355,8 @@ export const layer = Layer.effect( ) yield* db((db) => db.delete(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).run()) + s.shared.delete(sessionID) + s.queue.delete(sessionID) }) return Service.of({ init, url, request, create, remove }) From 6af8ab0df201e245fac503aa8822750662ebf6b9 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 16:20:57 -0400 Subject: [PATCH 127/335] docs(http-api): refresh bridge inventory and clarify Schema.Class vs Struct (#23164) --- packages/opencode/specs/effect/http-api.md | 77 ++++++++++++++++------ 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index 71b50250ed..09cb86a000 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -189,10 +189,46 @@ Ordering for a route-group migration: SDK shape rule: -- every schema migration must preserve the generated SDK output byte-for-byte -- `Schema.Class` emits a named `$ref` in OpenAPI via its identifier — use it only for types that already had `.meta({ ref })` in the old Zod schema -- inner / nested types that were anonymous in the old Zod schema should stay as `Schema.Struct` (not `Schema.Class`) to avoid introducing new named components in the OpenAPI spec -- if a diff appears in `packages/sdk/js/src/v2/gen/types.gen.ts`, the migration introduced an unintended API surface change — fix it before merging +- every schema migration must preserve the generated SDK output byte-for-byte **unless the new ref is intentional** (see Schema.Class vs Schema.Struct below) +- if an unintended diff appears in `packages/sdk/js/src/v2/gen/types.gen.ts`, the migration introduced an unintended API surface change — fix it before merging + +### Schema.Class vs Schema.Struct + +The pattern choice determines whether a schema becomes a **named** export in the SDK or stays **anonymous inline**. + +**Schema.Class** emits a named `$ref` in OpenAPI via its identifier → produces a named `export type Foo = ...` in `types.gen.ts`: + +```ts +export class Info extends Schema.Class("FooConfig")({ ... }) { + static readonly zod = zod(this) +} +``` + +**Schema.Struct** stays anonymous and is inlined everywhere it is referenced: + +```ts +export const Info = Schema.Struct({ ... }).pipe( + withStatics((s) => ({ zod: zod(s) })), +) +export type Info = Schema.Schema.Type +``` + +When to use each: + +- Use **Schema.Class** when: + - the original Zod had `.meta({ ref: ... })` (preserve the existing named SDK type byte-for-byte) + - the schema is a top-level endpoint request or response (SDK consumers benefit from a stable importable name) +- Use **Schema.Struct** when: + - the type is only used as a nested field inside another named schema + - the original Zod was anonymous and promoting it would bloat SDK types with no import value + +Promoting a previously-anonymous schema to Schema.Class is acceptable when it is top-level or endpoint-facing, but call it out in the PR — it is an additive SDK change (`export type Foo = ...` newly appears) even if it preserves the JSON shape. + +Schemas that are **not** pure objects (enums, unions, records, tuples) cannot use Schema.Class. For those, add `.annotate({ identifier: "FooName" })` to get the same named-ref behavior: + +```ts +export const Action = Schema.Literals(["ask", "allow", "deny"]).annotate({ identifier: "PermissionActionConfig" }) +``` Temporary exception: @@ -365,17 +401,16 @@ Current instance route inventory: endpoints: `GET /question`, `POST /question/:requestID/reply`, `POST /question/:requestID/reject` - `permission` - `bridged` endpoints: `GET /permission`, `POST /permission/:requestID/reply` -- `provider` - `bridged` (partial) - bridged endpoint: `GET /provider/auth` - not yet ported: `GET /provider`, OAuth mutations -- `config` - `next` - best next endpoint: `GET /config/providers` +- `provider` - `bridged` + endpoints: `GET /provider`, `GET /provider/auth`, `POST /provider/:providerID/oauth/authorize`, `POST /provider/:providerID/oauth/callback` +- `config` - `bridged` (partial) + bridged endpoint: `GET /config/providers` later endpoint: `GET /config` defer `PATCH /config` for now -- `project` - `later` - best small reads: `GET /project`, `GET /project/current` +- `project` - `bridged` (partial) + bridged endpoints: `GET /project`, `GET /project/current` defer git-init mutation first -- `workspace` - `later` +- `workspace` - `next` best small reads: `GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status` defer create/remove mutations first - `file` - `later` @@ -393,12 +428,12 @@ Current instance route inventory: - `tui` - `defer` queue-style UI bridge, weak early `HttpApi` fit -Recommended near-term sequence after the first spike: +Recommended near-term sequence: -1. `provider` auth read endpoint -2. `config` providers read endpoint -3. `project` read endpoints -4. `workspace` read endpoints +1. `workspace` read endpoints (`GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`) +2. `config` full read endpoint (`GET /config`) +3. `file` JSON read endpoints +4. `mcp` JSON read endpoints ## Checklist @@ -411,8 +446,12 @@ Recommended near-term sequence after the first spike: - [x] gate behind `OPENCODE_EXPERIMENTAL_HTTPAPI` flag - [x] verify OTEL spans and HTTP logs flow to motel - [x] bridge question, permission, and provider auth routes -- [ ] port remaining provider endpoints (`GET /provider`, OAuth mutations) -- [ ] port `config` read endpoints +- [x] port remaining provider endpoints (`GET /provider`, OAuth mutations) +- [x] port `config` providers read endpoint +- [x] port `project` read endpoints (`GET /project`, `GET /project/current`) +- [ ] port `workspace` read endpoints +- [ ] port `GET /config` full read endpoint +- [ ] port `file` JSON read endpoints - [ ] decide when to remove the flag and make Effect routes the default ## Rule of thumb From 11fa257549a1094b43d25f014e54557ed6aab660 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 16:25:49 -0400 Subject: [PATCH 128/335] refactor(config): migrate mcp schemas to Effect Schema.Class (#23163) --- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/config/mcp.ts | 118 +++++++++--------- .../src/server/routes/instance/mcp.ts | 2 +- 3 files changed, 58 insertions(+), 64 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 459f76961a..258500d7bd 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -178,7 +178,7 @@ export const Info = z .record( z.string(), z.union([ - ConfigMCP.Info, + ConfigMCP.Info.zod, z .object({ enabled: z.boolean(), diff --git a/packages/opencode/src/config/mcp.ts b/packages/opencode/src/config/mcp.ts index 5036cd6e4f..8b77bc4c28 100644 --- a/packages/opencode/src/config/mcp.ts +++ b/packages/opencode/src/config/mcp.ts @@ -1,68 +1,62 @@ -import z from "zod" +import { Schema } from "effect" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" -export const Local = z - .object({ - type: z.literal("local").describe("Type of MCP server connection"), - command: z.string().array().describe("Command and arguments to run the MCP server"), - environment: z - .record(z.string(), z.string()) - .optional() - .describe("Environment variables to set when running the MCP server"), - enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), - timeout: z - .number() - .int() - .positive() - .optional() - .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), - }) - .strict() - .meta({ - ref: "McpLocalConfig", - }) +export class Local extends Schema.Class("McpLocalConfig")({ + type: Schema.Literal("local").annotate({ description: "Type of MCP server connection" }), + command: Schema.mutable(Schema.Array(Schema.String)).annotate({ + description: "Command and arguments to run the MCP server", + }), + environment: Schema.optional(Schema.Record(Schema.String, Schema.String)).annotate({ + description: "Environment variables to set when running the MCP server", + }), + enabled: Schema.optional(Schema.Boolean).annotate({ + description: "Enable or disable the MCP server on startup", + }), + timeout: Schema.optional(Schema.Number).annotate({ + description: "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.", + }), +}) { + static readonly zod = zod(this) +} -export const OAuth = z - .object({ - clientId: z - .string() - .optional() - .describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."), - clientSecret: z.string().optional().describe("OAuth client secret (if required by the authorization server)"), - scope: z.string().optional().describe("OAuth scopes to request during authorization"), - redirectUri: z - .string() - .optional() - .describe("OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback)."), - }) - .strict() - .meta({ - ref: "McpOAuthConfig", - }) -export type OAuth = z.infer +export class OAuth extends Schema.Class("McpOAuthConfig")({ + clientId: Schema.optional(Schema.String).annotate({ + description: "OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted.", + }), + clientSecret: Schema.optional(Schema.String).annotate({ + description: "OAuth client secret (if required by the authorization server)", + }), + scope: Schema.optional(Schema.String).annotate({ description: "OAuth scopes to request during authorization" }), + redirectUri: Schema.optional(Schema.String).annotate({ + description: "OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback).", + }), +}) { + static readonly zod = zod(this) +} -export const Remote = z - .object({ - type: z.literal("remote").describe("Type of MCP server connection"), - url: z.string().describe("URL of the remote MCP server"), - enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), - headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"), - oauth: z - .union([OAuth, z.literal(false)]) - .optional() - .describe("OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection."), - timeout: z - .number() - .int() - .positive() - .optional() - .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), - }) - .strict() - .meta({ - ref: "McpRemoteConfig", - }) +export class Remote extends Schema.Class("McpRemoteConfig")({ + type: Schema.Literal("remote").annotate({ description: "Type of MCP server connection" }), + url: Schema.String.annotate({ description: "URL of the remote MCP server" }), + enabled: Schema.optional(Schema.Boolean).annotate({ + description: "Enable or disable the MCP server on startup", + }), + headers: Schema.optional(Schema.Record(Schema.String, Schema.String)).annotate({ + description: "Headers to send with the request", + }), + oauth: Schema.optional(Schema.Union([OAuth, Schema.Literal(false)])).annotate({ + description: "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.", + }), + timeout: Schema.optional(Schema.Number).annotate({ + description: "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.", + }), +}) { + static readonly zod = zod(this) +} -export const Info = z.discriminatedUnion("type", [Local, Remote]) -export type Info = z.infer +export const Info = Schema.Union([Local, Remote]) + .annotate({ discriminator: "type" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Info = Schema.Schema.Type export * as ConfigMCP from "./mcp" diff --git a/packages/opencode/src/server/routes/instance/mcp.ts b/packages/opencode/src/server/routes/instance/mcp.ts index 197185bde0..b42cfb5314 100644 --- a/packages/opencode/src/server/routes/instance/mcp.ts +++ b/packages/opencode/src/server/routes/instance/mcp.ts @@ -54,7 +54,7 @@ export const McpRoutes = lazy(() => "json", z.object({ name: z.string(), - config: ConfigMCP.Info, + config: ConfigMCP.Info.zod, }), ), async (c) => { From 54435325b6adf292d720885622f1282eaed98c92 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 20:26:43 +0000 Subject: [PATCH 129/335] chore: generate --- packages/sdk/openapi.json | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 3b811f2fa9..71e78307cd 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -11428,13 +11428,10 @@ }, "timeout": { "description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.", - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991 + "type": "number" } }, - "required": ["type", "command"], - "additionalProperties": false + "required": ["type", "command"] }, "McpOAuthConfig": { "type": "object", @@ -11455,8 +11452,7 @@ "description": "OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback).", "type": "string" } - }, - "additionalProperties": false + } }, "McpRemoteConfig": { "type": "object", @@ -11498,13 +11494,10 @@ }, "timeout": { "description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.", - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991 + "type": "number" } }, - "required": ["type", "url"], - "additionalProperties": false + "required": ["type", "url"] }, "LayoutConfig": { "description": "@deprecated Always uses stretch layout.", From 650a13a6908e23c30e07329f9f3ec816d7a6f2a7 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 16:34:47 -0400 Subject: [PATCH 130/335] refactor(config): migrate lsp schemas to Effect Schema (#23167) --- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/config/lsp.ts | 58 ++++++++++++++------------ 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 258500d7bd..039b0598da 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -189,7 +189,7 @@ export const Info = z .optional() .describe("MCP (Model Context Protocol) server configurations"), formatter: ConfigFormatter.Info.optional(), - lsp: ConfigLSP.Info.optional(), + lsp: ConfigLSP.Info.zod.optional(), instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"), layout: Layout.optional().describe("@deprecated Always uses stretch layout."), permission: ConfigPermission.Info.optional(), diff --git a/packages/opencode/src/config/lsp.ts b/packages/opencode/src/config/lsp.ts index 5530a5be56..94a6d4bc56 100644 --- a/packages/opencode/src/config/lsp.ts +++ b/packages/opencode/src/config/lsp.ts @@ -1,37 +1,43 @@ export * as ConfigLSP from "./lsp" -import z from "zod" +import { Schema } from "effect" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" import * as LSPServer from "../lsp/server" -export const Disabled = z.object({ - disabled: z.literal(true), -}) +export const Disabled = Schema.Struct({ + disabled: Schema.Literal(true), +}).pipe(withStatics((s) => ({ zod: zod(s) }))) -export const Entry = z.union([ +export const Entry = Schema.Union([ Disabled, - z.object({ - command: z.array(z.string()), - extensions: z.array(z.string()).optional(), - disabled: z.boolean().optional(), - env: z.record(z.string(), z.string()).optional(), - initialization: z.record(z.string(), z.any()).optional(), + Schema.Struct({ + command: Schema.mutable(Schema.Array(Schema.String)), + extensions: Schema.optional(Schema.mutable(Schema.Array(Schema.String))), + disabled: Schema.optional(Schema.Boolean), + env: Schema.optional(Schema.Record(Schema.String, Schema.String)), + initialization: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }), -]) +]).pipe(withStatics((s) => ({ zod: zod(s) }))) -export const Info = z.union([z.boolean(), z.record(z.string(), Entry)]).refine( - (data) => { - if (typeof data === "boolean") return true - const serverIds = new Set(Object.values(LSPServer).map((server) => server.id)) +export const Info = Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Entry)]).pipe( + withStatics((s) => ({ + zod: zod(s).refine( + (data) => { + if (typeof data === "boolean") return true + const serverIds = new Set(Object.values(LSPServer).map((server) => server.id)) - return Object.entries(data).every(([id, config]) => { - if (config.disabled) return true - if (serverIds.has(id)) return true - return Boolean(config.extensions) - }) - }, - { - error: "For custom LSP servers, 'extensions' array is required.", - }, + return Object.entries(data).every(([id, config]) => { + if (config.disabled) return true + if (serverIds.has(id)) return true + return Boolean(config.extensions) + }) + }, + { + error: "For custom LSP servers, 'extensions' array is required.", + }, + ), + })), ) -export type Info = z.infer +export type Info = Schema.Schema.Type From d11268ece76d132108feda4bb91f0caacac51719 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 16:35:42 -0400 Subject: [PATCH 131/335] refactor(config): migrate permission Action/Object/Rule leaves to Effect Schema (#23168) --- packages/opencode/src/config/permission.ts | 63 +++++++++++----------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/packages/opencode/src/config/permission.ts b/packages/opencode/src/config/permission.ts index af01f6f2a3..7cfbaec01f 100644 --- a/packages/opencode/src/config/permission.ts +++ b/packages/opencode/src/config/permission.ts @@ -1,5 +1,8 @@ export * as ConfigPermission from "./permission" +import { Schema } from "effect" import z from "zod" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" const permissionPreprocess = (val: unknown) => { if (typeof val === "object" && val !== null && !Array.isArray(val)) { @@ -8,20 +11,20 @@ const permissionPreprocess = (val: unknown) => { return val } -export const Action = z.enum(["ask", "allow", "deny"]).meta({ - ref: "PermissionActionConfig", -}) -export type Action = z.infer +export const Action = Schema.Literals(["ask", "allow", "deny"]) + .annotate({ identifier: "PermissionActionConfig" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Action = Schema.Schema.Type -export const Object = z.record(z.string(), Action).meta({ - ref: "PermissionObjectConfig", -}) -export type Object = z.infer +export const Object = Schema.Record(Schema.String, Action) + .annotate({ identifier: "PermissionObjectConfig" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Object = Schema.Schema.Type -export const Rule = z.union([Action, Object]).meta({ - ref: "PermissionRuleConfig", -}) -export type Rule = z.infer +export const Rule = Schema.Union([Action, Object]) + .annotate({ identifier: "PermissionRuleConfig" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Rule = Schema.Schema.Type const transform = (x: unknown): Record => { if (typeof x === "string") return { "*": x as Action } @@ -41,25 +44,25 @@ export const Info = z z .object({ __originalKeys: z.string().array().optional(), - read: Rule.optional(), - edit: Rule.optional(), - glob: Rule.optional(), - grep: Rule.optional(), - list: Rule.optional(), - bash: Rule.optional(), - task: Rule.optional(), - external_directory: Rule.optional(), - todowrite: Action.optional(), - question: Action.optional(), - webfetch: Action.optional(), - websearch: Action.optional(), - codesearch: Action.optional(), - lsp: Rule.optional(), - doom_loop: Action.optional(), - skill: Rule.optional(), + read: Rule.zod.optional(), + edit: Rule.zod.optional(), + glob: Rule.zod.optional(), + grep: Rule.zod.optional(), + list: Rule.zod.optional(), + bash: Rule.zod.optional(), + task: Rule.zod.optional(), + external_directory: Rule.zod.optional(), + todowrite: Action.zod.optional(), + question: Action.zod.optional(), + webfetch: Action.zod.optional(), + websearch: Action.zod.optional(), + codesearch: Action.zod.optional(), + lsp: Rule.zod.optional(), + doom_loop: Action.zod.optional(), + skill: Rule.zod.optional(), }) - .catchall(Rule) - .or(Action), + .catchall(Rule.zod) + .or(Action.zod), ) .transform(transform) .meta({ From 47f553f9ba9d98056fa5a20fd1a4e6d4d63df016 Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 17 Apr 2026 16:39:34 -0400 Subject: [PATCH 132/335] fix(core): more explicit routing to fix workspace instance issue (#23171) --- .../src/server/routes/control/index.ts | 2 -- .../src/server/routes/instance/index.ts | 5 ++--- packages/opencode/src/server/server.ts | 19 +++++++++++-------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/opencode/src/server/routes/control/index.ts b/packages/opencode/src/server/routes/control/index.ts index 3fd60636ff..60883274a5 100644 --- a/packages/opencode/src/server/routes/control/index.ts +++ b/packages/opencode/src/server/routes/control/index.ts @@ -7,7 +7,6 @@ import { Hono } from "hono" import { describeRoute, resolver, validator, openAPIRouteHandler } from "hono-openapi" import z from "zod" import { errors } from "../../error" -import { WorkspaceRoutes } from "./workspace" export function ControlPlaneRoutes(): Hono { const app = new Hono() @@ -158,5 +157,4 @@ export function ControlPlaneRoutes(): Hono { return c.json(true) }, ) - .route("/experimental/workspace", WorkspaceRoutes()) } diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index c0339fded7..ddb7e335ad 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -15,7 +15,6 @@ import { Command } from "@/command" import { QuestionRoutes } from "./question" import { PermissionRoutes } from "./permission" import { Flag } from "@/flag/flag" -import { WorkspaceID } from "@/control-plane/schema" import { ExperimentalHttpApiServer } from "./httpapi/server" import { ProjectRoutes } from "./project" import { SessionRoutes } from "./session" @@ -30,8 +29,8 @@ import { SyncRoutes } from "./sync" import { AppRuntime } from "@/effect/app-runtime" import { InstanceMiddleware } from "./middleware" -export const InstanceRoutes = (upgrade: UpgradeWebSocket, workspaceID?: WorkspaceID): Hono => { - const app = new Hono().use(InstanceMiddleware(workspaceID)) +export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { + const app = new Hono() if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) { const handler = ExperimentalHttpApiServer.webHandler().handler diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index f608c2b732..8b1f1aee10 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -14,6 +14,8 @@ import { ControlPlaneRoutes } from "./routes/control" import { UIRoutes } from "./routes/ui" import { GlobalRoutes } from "./routes/global" import { WorkspaceRouterMiddleware } from "./workspace" +import { InstanceMiddleware } from "./routes/instance/middleware" +import { WorkspaceRoutes } from "./routes/control/workspace" // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85 globalThis.AI_SDK_LOG_WARNINGS = false @@ -45,14 +47,9 @@ function create(opts: { cors?: string[] }) { if (Flag.OPENCODE_WORKSPACE_ID) { return { app: app + .use(InstanceMiddleware(Flag.OPENCODE_WORKSPACE_ID ? WorkspaceID.make(Flag.OPENCODE_WORKSPACE_ID) : undefined)) .use(FenceMiddleware) - .route( - "/", - InstanceRoutes( - runtime.upgradeWebSocket, - Flag.OPENCODE_WORKSPACE_ID ? WorkspaceID.make(Flag.OPENCODE_WORKSPACE_ID) : undefined, - ), - ), + .route("/", InstanceRoutes(runtime.upgradeWebSocket)), runtime, } } @@ -60,7 +57,13 @@ function create(opts: { cors?: string[] }) { return { app: app .route("/", ControlPlaneRoutes()) - .use(WorkspaceRouterMiddleware(runtime.upgradeWebSocket)) + .route( + "/", + new Hono() + .use(InstanceMiddleware()) + .route("/experimental/workspace", WorkspaceRoutes()) + .use(WorkspaceRouterMiddleware(runtime.upgradeWebSocket)), + ) .route("/", InstanceRoutes(runtime.upgradeWebSocket)) .route("/", UIRoutes()), runtime, From e7686dbd643241f7e4ebf4a5e37e5ef33ad7797f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 16:46:05 -0400 Subject: [PATCH 133/335] feat(effect-zod): translate Schema.check filters into zod .superRefine + promote LSP refinement to Effect layer (#23173) --- packages/opencode/src/config/lsp.ts | 38 ++++---- packages/opencode/src/util/effect-zod.ts | 23 ++++- packages/opencode/test/config/lsp.test.ts | 87 +++++++++++++++++++ .../opencode/test/util/effect-zod.test.ts | 61 +++++++++++++ 4 files changed, 190 insertions(+), 19 deletions(-) create mode 100644 packages/opencode/test/config/lsp.test.ts diff --git a/packages/opencode/src/config/lsp.ts b/packages/opencode/src/config/lsp.ts index 94a6d4bc56..6d61c6b8e3 100644 --- a/packages/opencode/src/config/lsp.ts +++ b/packages/opencode/src/config/lsp.ts @@ -20,24 +20,26 @@ export const Entry = Schema.Union([ }), ]).pipe(withStatics((s) => ({ zod: zod(s) }))) -export const Info = Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Entry)]).pipe( - withStatics((s) => ({ - zod: zod(s).refine( - (data) => { - if (typeof data === "boolean") return true - const serverIds = new Set(Object.values(LSPServer).map((server) => server.id)) - - return Object.entries(data).every(([id, config]) => { - if (config.disabled) return true - if (serverIds.has(id)) return true - return Boolean(config.extensions) - }) - }, - { - error: "For custom LSP servers, 'extensions' array is required.", - }, - ), - })), +/** + * For custom (non-builtin) LSP server entries, `extensions` is required so the + * client knows which files the server should attach to. Builtin server IDs and + * explicitly disabled entries are exempt. + */ +export const requiresExtensionsForCustomServers = Schema.makeFilter>>( + (data) => { + if (typeof data === "boolean") return undefined + const serverIds = new Set(Object.values(LSPServer).map((server) => server.id)) + const ok = Object.entries(data).every(([id, config]) => { + if ("disabled" in config && config.disabled) return true + if (serverIds.has(id)) return true + return "extensions" in config && Boolean(config.extensions) + }) + return ok ? undefined : "For custom LSP servers, 'extensions' array is required." + }, ) +export const Info = Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Entry)]) + .check(requiresExtensionsForCustomServers) + .pipe(withStatics((s) => ({ zod: zod(s) }))) + export type Info = Schema.Schema.Type diff --git a/packages/opencode/src/util/effect-zod.ts b/packages/opencode/src/util/effect-zod.ts index 6e99fd4688..c3240deaac 100644 --- a/packages/opencode/src/util/effect-zod.ts +++ b/packages/opencode/src/util/effect-zod.ts @@ -16,13 +16,34 @@ function walk(ast: SchemaAST.AST): z.ZodTypeAny { const override = (ast.annotations as any)?.[ZodOverride] as z.ZodTypeAny | undefined if (override) return override - const out = body(ast) + let out = body(ast) + for (const check of ast.checks ?? []) { + out = applyCheck(out, check, ast) + } const desc = SchemaAST.resolveDescription(ast) const ref = SchemaAST.resolveIdentifier(ast) const next = desc ? out.describe(desc) : out return ref ? next.meta({ ref }) : next } +function applyCheck(out: z.ZodTypeAny, check: SchemaAST.Check, ast: SchemaAST.AST): z.ZodTypeAny { + if (check._tag === "FilterGroup") { + return check.checks.reduce((acc, sub) => applyCheck(acc, sub, ast), out) + } + return out.superRefine((value, ctx) => { + const issue = check.run(value, ast, {} as any) + if (!issue) return + const message = issueMessage(issue) ?? (check.annotations as any)?.message ?? "Validation failed" + ctx.addIssue({ code: "custom", message }) + }) +} + +function issueMessage(issue: any): string | undefined { + if (typeof issue?.annotations?.message === "string") return issue.annotations.message + if (typeof issue?.message === "string") return issue.message + return undefined +} + function body(ast: SchemaAST.AST): z.ZodTypeAny { if (SchemaAST.isOptional(ast)) return opt(ast) diff --git a/packages/opencode/test/config/lsp.test.ts b/packages/opencode/test/config/lsp.test.ts new file mode 100644 index 0000000000..1d24fe124d --- /dev/null +++ b/packages/opencode/test/config/lsp.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, test } from "bun:test" +import { Schema } from "effect" +import { ConfigLSP } from "../../src/config/lsp" + +// The LSP config refinement enforces: any custom (non-builtin) LSP server +// entry must declare an `extensions` array so the client knows which files +// the server should attach to. Builtin server IDs and explicitly disabled +// entries are exempt. +// +// Both validation paths must honor this rule: +// - `Schema.decodeUnknownSync(ConfigLSP.Info)` (Effect layer) +// - `ConfigLSP.Info.zod.parse(...)` (derived Zod) +// +// `typescript` is a builtin server id (see src/lsp/server.ts). +describe("ConfigLSP.Info refinement", () => { + const decodeEffect = Schema.decodeUnknownSync(ConfigLSP.Info) + + describe("accepted inputs", () => { + test("true and false pass (top-level toggle)", () => { + expect(decodeEffect(true)).toBe(true) + expect(decodeEffect(false)).toBe(false) + expect(ConfigLSP.Info.zod.parse(true)).toBe(true) + expect(ConfigLSP.Info.zod.parse(false)).toBe(false) + }) + + test("builtin server with no extensions passes", () => { + const input = { typescript: { command: ["typescript-language-server", "--stdio"] } } + expect(decodeEffect(input)).toEqual(input) + expect(ConfigLSP.Info.zod.parse(input)).toEqual(input) + }) + + test("custom server WITH extensions passes", () => { + const input = { + "my-lsp": { command: ["my-lsp-bin"], extensions: [".ml"] }, + } + expect(decodeEffect(input)).toEqual(input) + expect(ConfigLSP.Info.zod.parse(input)).toEqual(input) + }) + + test("disabled custom server passes (no extensions needed)", () => { + const input = { "my-lsp": { disabled: true as const } } + expect(decodeEffect(input)).toEqual(input) + expect(ConfigLSP.Info.zod.parse(input)).toEqual(input) + }) + + test("mix of builtin and custom with extensions passes", () => { + const input = { + typescript: { command: ["typescript-language-server", "--stdio"] }, + "my-lsp": { command: ["my-lsp-bin"], extensions: [".ml"] }, + } + expect(decodeEffect(input)).toEqual(input) + expect(ConfigLSP.Info.zod.parse(input)).toEqual(input) + }) + }) + + describe("rejected inputs", () => { + const expectedMessage = "For custom LSP servers, 'extensions' array is required." + + test("custom server WITHOUT extensions fails via Effect decode", () => { + expect(() => decodeEffect({ "my-lsp": { command: ["my-lsp-bin"] } })).toThrow(expectedMessage) + }) + + test("custom server WITHOUT extensions fails via derived Zod", () => { + const result = ConfigLSP.Info.zod.safeParse({ "my-lsp": { command: ["my-lsp-bin"] } }) + expect(result.success).toBe(false) + expect(result.error!.issues.some((i) => i.message === expectedMessage)).toBe(true) + }) + + test("custom server with empty extensions array fails (extensions must be non-empty-truthy)", () => { + // Boolean(['']) is true, so a non-empty array of strings is fine. + // Boolean([]) is also true in JS, so empty arrays are accepted by the + // refinement. This test documents current behavior. + const input = { "my-lsp": { command: ["my-lsp-bin"], extensions: [] } } + expect(decodeEffect(input)).toEqual(input) + expect(ConfigLSP.Info.zod.parse(input)).toEqual(input) + }) + + test("custom server without extensions mixed with a valid builtin still fails", () => { + const input = { + typescript: { command: ["typescript-language-server", "--stdio"] }, + "my-lsp": { command: ["my-lsp-bin"] }, + } + expect(() => decodeEffect(input)).toThrow(expectedMessage) + expect(ConfigLSP.Info.zod.safeParse(input).success).toBe(false) + }) + }) +}) diff --git a/packages/opencode/test/util/effect-zod.test.ts b/packages/opencode/test/util/effect-zod.test.ts index 7f7249514d..2a5cc45f8d 100644 --- a/packages/opencode/test/util/effect-zod.test.ts +++ b/packages/opencode/test/util/effect-zod.test.ts @@ -186,4 +186,65 @@ describe("util.effect-zod", () => { const schema = json(zod(Parent)) as any expect(schema.properties.sessionID).toEqual({ type: "string", pattern: "^ses.*" }) }) + + describe("Schema.check translation", () => { + test("filter returning string triggers refinement with that message", () => { + const isEven = Schema.makeFilter((n: number) => + n % 2 === 0 ? undefined : "expected an even number", + ) + const schema = zod(Schema.Number.check(isEven)) + + expect(schema.parse(4)).toBe(4) + const result = schema.safeParse(3) + expect(result.success).toBe(false) + expect(result.error!.issues[0].message).toBe("expected an even number") + }) + + test("filter returning false triggers refinement with fallback message", () => { + const nonEmpty = Schema.makeFilter((s: string) => s.length > 0) + const schema = zod(Schema.String.check(nonEmpty)) + + expect(schema.parse("hi")).toBe("hi") + const result = schema.safeParse("") + expect(result.success).toBe(false) + expect(result.error!.issues[0].message).toMatch(/./) + }) + + test("filter returning undefined passes validation", () => { + const alwaysOk = Schema.makeFilter(() => undefined) + const schema = zod(Schema.Number.check(alwaysOk)) + + expect(schema.parse(42)).toBe(42) + }) + + test("annotations.message on the filter is used when filter returns false", () => { + const positive = Schema.makeFilter( + (n: number) => n > 0, + { message: "must be positive" }, + ) + const schema = zod(Schema.Number.check(positive)) + + const result = schema.safeParse(-1) + expect(result.success).toBe(false) + expect(result.error!.issues[0].message).toBe("must be positive") + }) + + test("cross-field check on a record flags missing key", () => { + const hasKey = Schema.makeFilter( + (data: Record) => + "required" in data ? undefined : "missing 'required' key", + ) + const schema = zod( + Schema.Record(Schema.String, Schema.Struct({ enabled: Schema.Boolean })).check(hasKey), + ) + + expect(schema.parse({ required: { enabled: true } })).toEqual({ + required: { enabled: true }, + }) + + const result = schema.safeParse({ other: { enabled: true } }) + expect(result.success).toBe(false) + expect(result.error!.issues[0].message).toBe("missing 'required' key") + }) + }) }) From dc16013b4f0aa5e4d3eb433046965e1274da3990 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 20:47:05 +0000 Subject: [PATCH 134/335] chore: generate --- packages/opencode/src/config/lsp.ts | 24 +++++++++---------- .../opencode/test/util/effect-zod.test.ts | 18 ++++---------- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/packages/opencode/src/config/lsp.ts b/packages/opencode/src/config/lsp.ts index 6d61c6b8e3..1cf93177e4 100644 --- a/packages/opencode/src/config/lsp.ts +++ b/packages/opencode/src/config/lsp.ts @@ -25,18 +25,18 @@ export const Entry = Schema.Union([ * client knows which files the server should attach to. Builtin server IDs and * explicitly disabled entries are exempt. */ -export const requiresExtensionsForCustomServers = Schema.makeFilter>>( - (data) => { - if (typeof data === "boolean") return undefined - const serverIds = new Set(Object.values(LSPServer).map((server) => server.id)) - const ok = Object.entries(data).every(([id, config]) => { - if ("disabled" in config && config.disabled) return true - if (serverIds.has(id)) return true - return "extensions" in config && Boolean(config.extensions) - }) - return ok ? undefined : "For custom LSP servers, 'extensions' array is required." - }, -) +export const requiresExtensionsForCustomServers = Schema.makeFilter< + boolean | Record> +>((data) => { + if (typeof data === "boolean") return undefined + const serverIds = new Set(Object.values(LSPServer).map((server) => server.id)) + const ok = Object.entries(data).every(([id, config]) => { + if ("disabled" in config && config.disabled) return true + if (serverIds.has(id)) return true + return "extensions" in config && Boolean(config.extensions) + }) + return ok ? undefined : "For custom LSP servers, 'extensions' array is required." +}) export const Info = Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Entry)]) .check(requiresExtensionsForCustomServers) diff --git a/packages/opencode/test/util/effect-zod.test.ts b/packages/opencode/test/util/effect-zod.test.ts index 2a5cc45f8d..1c84c96f3a 100644 --- a/packages/opencode/test/util/effect-zod.test.ts +++ b/packages/opencode/test/util/effect-zod.test.ts @@ -189,9 +189,7 @@ describe("util.effect-zod", () => { describe("Schema.check translation", () => { test("filter returning string triggers refinement with that message", () => { - const isEven = Schema.makeFilter((n: number) => - n % 2 === 0 ? undefined : "expected an even number", - ) + const isEven = Schema.makeFilter((n: number) => (n % 2 === 0 ? undefined : "expected an even number")) const schema = zod(Schema.Number.check(isEven)) expect(schema.parse(4)).toBe(4) @@ -218,10 +216,7 @@ describe("util.effect-zod", () => { }) test("annotations.message on the filter is used when filter returns false", () => { - const positive = Schema.makeFilter( - (n: number) => n > 0, - { message: "must be positive" }, - ) + const positive = Schema.makeFilter((n: number) => n > 0, { message: "must be positive" }) const schema = zod(Schema.Number.check(positive)) const result = schema.safeParse(-1) @@ -230,13 +225,10 @@ describe("util.effect-zod", () => { }) test("cross-field check on a record flags missing key", () => { - const hasKey = Schema.makeFilter( - (data: Record) => - "required" in data ? undefined : "missing 'required' key", - ) - const schema = zod( - Schema.Record(Schema.String, Schema.Struct({ enabled: Schema.Boolean })).check(hasKey), + const hasKey = Schema.makeFilter((data: Record) => + "required" in data ? undefined : "missing 'required' key", ) + const schema = zod(Schema.Record(Schema.String, Schema.Struct({ enabled: Schema.Boolean })).check(hasKey)) expect(schema.parse({ required: { enabled: true } })).toEqual({ required: { enabled: true }, From b1307d5c2a41c12d6696b31fe46bea7882993753 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 16:49:36 -0400 Subject: [PATCH 135/335] refactor(config): migrate skills, formatter, console-state to Effect Schema (#23162) --- packages/opencode/src/config/config.ts | 4 ++-- packages/opencode/src/config/console-state.ts | 21 +++++++++--------- packages/opencode/src/config/formatter.ts | 22 +++++++++++-------- packages/opencode/src/config/skills.ts | 21 ++++++++++-------- .../server/routes/instance/experimental.ts | 2 +- packages/sdk/js/src/v2/gen/types.gen.ts | 12 +++++----- 6 files changed, 46 insertions(+), 36 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 039b0598da..261c5acab4 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -100,7 +100,7 @@ export const Info = z .record(z.string(), ConfigCommand.Info) .optional() .describe("Command configuration, see https://opencode.ai/docs/commands"), - skills: ConfigSkills.Info.optional().describe("Additional skill folder paths"), + skills: ConfigSkills.Info.zod.optional().describe("Additional skill folder paths"), watcher: z .object({ ignore: z.array(z.string()).optional(), @@ -188,7 +188,7 @@ export const Info = z ) .optional() .describe("MCP (Model Context Protocol) server configurations"), - formatter: ConfigFormatter.Info.optional(), + formatter: ConfigFormatter.Info.zod.optional(), lsp: ConfigLSP.Info.zod.optional(), instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"), layout: Layout.optional().describe("@deprecated Always uses stretch layout."), diff --git a/packages/opencode/src/config/console-state.ts b/packages/opencode/src/config/console-state.ts index cf96a4e305..08668afe4e 100644 --- a/packages/opencode/src/config/console-state.ts +++ b/packages/opencode/src/config/console-state.ts @@ -1,15 +1,16 @@ -import z from "zod" +import { Schema } from "effect" +import { zod } from "@/util/effect-zod" -export const ConsoleState = z.object({ - consoleManagedProviders: z.array(z.string()), - activeOrgName: z.string().optional(), - switchableOrgCount: z.number().int().nonnegative(), -}) +export class ConsoleState extends Schema.Class("ConsoleState")({ + consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)), + activeOrgName: Schema.optional(Schema.String), + switchableOrgCount: Schema.Number, +}) { + static readonly zod = zod(this) +} -export type ConsoleState = z.infer - -export const emptyConsoleState: ConsoleState = { +export const emptyConsoleState: ConsoleState = ConsoleState.make({ consoleManagedProviders: [], activeOrgName: undefined, switchableOrgCount: 0, -} +}) diff --git a/packages/opencode/src/config/formatter.ts b/packages/opencode/src/config/formatter.ts index 93b87f0281..8c1f09a247 100644 --- a/packages/opencode/src/config/formatter.ts +++ b/packages/opencode/src/config/formatter.ts @@ -1,13 +1,17 @@ export * as ConfigFormatter from "./formatter" -import z from "zod" +import { Schema } from "effect" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" -export const Entry = z.object({ - disabled: z.boolean().optional(), - command: z.array(z.string()).optional(), - environment: z.record(z.string(), z.string()).optional(), - extensions: z.array(z.string()).optional(), -}) +export const Entry = Schema.Struct({ + disabled: Schema.optional(Schema.Boolean), + command: Schema.optional(Schema.mutable(Schema.Array(Schema.String))), + environment: Schema.optional(Schema.Record(Schema.String, Schema.String)), + extensions: Schema.optional(Schema.mutable(Schema.Array(Schema.String))), +}).pipe(withStatics((s) => ({ zod: zod(s) }))) -export const Info = z.union([z.boolean(), z.record(z.string(), Entry)]) -export type Info = z.infer +export const Info = Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Entry)]).pipe( + withStatics((s) => ({ zod: zod(s) })), +) +export type Info = Schema.Schema.Type diff --git a/packages/opencode/src/config/skills.ts b/packages/opencode/src/config/skills.ts index 38cbf99e7d..f29d854f50 100644 --- a/packages/opencode/src/config/skills.ts +++ b/packages/opencode/src/config/skills.ts @@ -1,13 +1,16 @@ -import z from "zod" +import { Schema } from "effect" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" -export const Info = z.object({ - paths: z.array(z.string()).optional().describe("Additional paths to skill folders"), - urls: z - .array(z.string()) - .optional() - .describe("URLs to fetch skills from (e.g., https://example.com/.well-known/skills/)"), -}) +export const Info = Schema.Struct({ + paths: Schema.optional(Schema.Array(Schema.String)).annotate({ + description: "Additional paths to skill folders", + }), + urls: Schema.optional(Schema.Array(Schema.String)).annotate({ + description: "URLs to fetch skills from (e.g., https://example.com/.well-known/skills/)", + }), +}).pipe(withStatics((s) => ({ zod: zod(s) }))) -export type Info = z.infer +export type Info = Schema.Schema.Type export * as ConfigSkills from "./skills" diff --git a/packages/opencode/src/server/routes/instance/experimental.ts b/packages/opencode/src/server/routes/instance/experimental.ts index f7ecc8255b..d5659346ef 100644 --- a/packages/opencode/src/server/routes/instance/experimental.ts +++ b/packages/opencode/src/server/routes/instance/experimental.ts @@ -49,7 +49,7 @@ export const ExperimentalRoutes = lazy(() => description: "Active Console provider metadata", content: { "application/json": { - schema: resolver(ConsoleState), + schema: resolver(ConsoleState.zod), }, }, }, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 5698cba54f..72a383a608 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1807,6 +1807,12 @@ export type Provider = { } } +export type ConsoleState = { + consoleManagedProviders: Array + activeOrgName?: string + switchableOrgCount: number +} + export type ToolIds = Array export type ToolListItem = { @@ -2933,11 +2939,7 @@ export type ExperimentalConsoleGetResponses = { /** * Active Console provider metadata */ - 200: { - consoleManagedProviders: Array - activeOrgName?: string - switchableOrgCount: number - } + 200: ConsoleState } export type ExperimentalConsoleGetResponse = ExperimentalConsoleGetResponses[keyof ExperimentalConsoleGetResponses] From ed0f0225025336ad84c04210850fcd7bfb38f221 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 20:50:37 +0000 Subject: [PATCH 136/335] chore: generate --- packages/sdk/openapi.json | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 71e78307cd..c3fd00356c 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -1607,24 +1607,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "consoleManagedProviders": { - "type": "array", - "items": { - "type": "string" - } - }, - "activeOrgName": { - "type": "string" - }, - "switchableOrgCount": { - "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 - } - }, - "required": ["consoleManagedProviders", "switchableOrgCount"] + "$ref": "#/components/schemas/ConsoleState" } } } @@ -12359,6 +12342,24 @@ }, "required": ["id", "name", "source", "env", "options", "models"] }, + "ConsoleState": { + "type": "object", + "properties": { + "consoleManagedProviders": { + "type": "array", + "items": { + "type": "string" + } + }, + "activeOrgName": { + "type": "string" + }, + "switchableOrgCount": { + "type": "number" + } + }, + "required": ["consoleManagedProviders", "switchableOrgCount"] + }, "ToolIDs": { "type": "array", "items": { From 999d8651aab7f9531539f686f685375fcbb19437 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 16:52:40 -0400 Subject: [PATCH 137/335] feat(server): wrap remaining route handlers in request spans (#23169) --- .../src/server/routes/instance/config.ts | 13 +- .../server/routes/instance/experimental.ts | 170 ++++---- .../src/server/routes/instance/file.ts | 88 ++-- .../src/server/routes/instance/index.ts | 87 ++-- .../src/server/routes/instance/mcp.ts | 80 ++-- .../src/server/routes/instance/permission.ts | 37 +- .../src/server/routes/instance/project.ts | 18 +- .../src/server/routes/instance/provider.ts | 117 +++-- .../src/server/routes/instance/pty.ts | 69 ++- .../src/server/routes/instance/question.ts | 47 +- .../src/server/routes/instance/session.ts | 411 +++++++++--------- .../src/server/routes/instance/tui.ts | 8 +- packages/opencode/src/server/workspace.ts | 5 +- 13 files changed, 550 insertions(+), 600 deletions(-) diff --git a/packages/opencode/src/server/routes/instance/config.ts b/packages/opencode/src/server/routes/instance/config.ts index 235f5682e2..7f368cd31c 100644 --- a/packages/opencode/src/server/routes/instance/config.ts +++ b/packages/opencode/src/server/routes/instance/config.ts @@ -5,7 +5,6 @@ import { Config } from "@/config" import { Provider } from "@/provider" import { errors } from "../../error" import { lazy } from "@/util/lazy" -import { AppRuntime } from "@/effect/app-runtime" import { jsonRequest } from "./trace" export const ConfigRoutes = lazy(() => @@ -52,11 +51,13 @@ export const ConfigRoutes = lazy(() => }, }), validator("json", Config.Info), - async (c) => { - const config = c.req.valid("json") - await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.update(config))) - return c.json(config) - }, + async (c) => + jsonRequest("ConfigRoutes.update", c, function* () { + const config = c.req.valid("json") + const cfg = yield* Config.Service + yield* cfg.update(config) + return config + }), ) .get( "/providers", diff --git a/packages/opencode/src/server/routes/instance/experimental.ts b/packages/opencode/src/server/routes/instance/experimental.ts index d5659346ef..9c86494987 100644 --- a/packages/opencode/src/server/routes/instance/experimental.ts +++ b/packages/opencode/src/server/routes/instance/experimental.ts @@ -12,11 +12,11 @@ import { Config } from "@/config" import { ConsoleState } from "@/config/console-state" import { Account } from "@/account/account" import { AccountID, OrgID } from "@/account/schema" -import { AppRuntime } from "@/effect/app-runtime" import { errors } from "../../error" import { lazy } from "@/util/lazy" import { Effect, Option } from "effect" import { Agent } from "@/agent/agent" +import { jsonRequest, runRequest } from "./trace" const ConsoleOrgOption = z.object({ accountID: z.string(), @@ -55,22 +55,18 @@ export const ExperimentalRoutes = lazy(() => }, }, }), - async (c) => { - const result = await AppRuntime.runPromise( - Effect.gen(function* () { - const config = yield* Config.Service - const account = yield* Account.Service - const [state, groups] = yield* Effect.all([config.getConsoleState(), account.orgsByAccount()], { - concurrency: "unbounded", - }) - return { - ...state, - switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0), - } - }), - ) - return c.json(result) - }, + async (c) => + jsonRequest("ExperimentalRoutes.console.get", c, function* () { + const config = yield* Config.Service + const account = yield* Account.Service + const [state, groups] = yield* Effect.all([config.getConsoleState(), account.orgsByAccount()], { + concurrency: "unbounded", + }) + return { + ...state, + switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0), + } + }), ) .get( "/console/orgs", @@ -89,28 +85,25 @@ export const ExperimentalRoutes = lazy(() => }, }, }), - async (c) => { - const orgs = await AppRuntime.runPromise( - Effect.gen(function* () { - const account = yield* Account.Service - const [groups, active] = yield* Effect.all([account.orgsByAccount(), account.active()], { - concurrency: "unbounded", - }) - const info = Option.getOrUndefined(active) - return groups.flatMap((group) => - group.orgs.map((org) => ({ - accountID: group.account.id, - accountEmail: group.account.email, - accountUrl: group.account.url, - orgID: org.id, - orgName: org.name, - active: !!info && info.id === group.account.id && info.active_org_id === org.id, - })), - ) - }), - ) - return c.json({ orgs }) - }, + async (c) => + jsonRequest("ExperimentalRoutes.console.listOrgs", c, function* () { + const account = yield* Account.Service + const [groups, active] = yield* Effect.all([account.orgsByAccount(), account.active()], { + concurrency: "unbounded", + }) + const info = Option.getOrUndefined(active) + const orgs = groups.flatMap((group) => + group.orgs.map((org) => ({ + accountID: group.account.id, + accountEmail: group.account.email, + accountUrl: group.account.url, + orgID: org.id, + orgName: org.name, + active: !!info && info.id === group.account.id && info.active_org_id === org.id, + })), + ) + return { orgs } + }), ) .post( "/console/switch", @@ -130,16 +123,13 @@ export const ExperimentalRoutes = lazy(() => }, }), validator("json", ConsoleSwitchBody), - async (c) => { - const body = c.req.valid("json") - await AppRuntime.runPromise( - Effect.gen(function* () { - const account = yield* Account.Service - yield* account.use(AccountID.make(body.accountID), Option.some(OrgID.make(body.orgID))) - }), - ) - return c.json(true) - }, + async (c) => + jsonRequest("ExperimentalRoutes.console.switchOrg", c, function* () { + const body = c.req.valid("json") + const account = yield* Account.Service + yield* account.use(AccountID.make(body.accountID), Option.some(OrgID.make(body.orgID))) + return true + }), ) .get( "/tool/ids", @@ -160,15 +150,11 @@ export const ExperimentalRoutes = lazy(() => ...errors(400), }, }), - async (c) => { - const ids = await AppRuntime.runPromise( - Effect.gen(function* () { - const registry = yield* ToolRegistry.Service - return yield* registry.ids() - }), - ) - return c.json(ids) - }, + async (c) => + jsonRequest("ExperimentalRoutes.tool.ids", c, function* () { + const registry = yield* ToolRegistry.Service + return yield* registry.ids() + }), ) .get( "/tool", @@ -210,7 +196,9 @@ export const ExperimentalRoutes = lazy(() => ), async (c) => { const { provider, model } = c.req.valid("query") - const tools = await AppRuntime.runPromise( + const tools = await runRequest( + "ExperimentalRoutes.tool.list", + c, Effect.gen(function* () { const agents = yield* Agent.Service const registry = yield* ToolRegistry.Service @@ -249,11 +237,12 @@ export const ExperimentalRoutes = lazy(() => }, }), validator("json", Worktree.CreateInput.optional()), - async (c) => { - const body = c.req.valid("json") - const worktree = await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.create(body))) - return c.json(worktree) - }, + async (c) => + jsonRequest("ExperimentalRoutes.worktree.create", c, function* () { + const body = c.req.valid("json") + const svc = yield* Worktree.Service + return yield* svc.create(body) + }), ) .get( "/worktree", @@ -272,10 +261,11 @@ export const ExperimentalRoutes = lazy(() => }, }, }), - async (c) => { - const sandboxes = await AppRuntime.runPromise(Project.Service.use((svc) => svc.sandboxes(Instance.project.id))) - return c.json(sandboxes) - }, + async (c) => + jsonRequest("ExperimentalRoutes.worktree.list", c, function* () { + const svc = yield* Project.Service + return yield* svc.sandboxes(Instance.project.id) + }), ) .delete( "/worktree", @@ -296,14 +286,15 @@ export const ExperimentalRoutes = lazy(() => }, }), validator("json", Worktree.RemoveInput), - async (c) => { - const body = c.req.valid("json") - await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.remove(body))) - await AppRuntime.runPromise( - Project.Service.use((svc) => svc.removeSandbox(Instance.project.id, body.directory)), - ) - return c.json(true) - }, + async (c) => + jsonRequest("ExperimentalRoutes.worktree.remove", c, function* () { + const body = c.req.valid("json") + const worktree = yield* Worktree.Service + const project = yield* Project.Service + yield* worktree.remove(body) + yield* project.removeSandbox(Instance.project.id, body.directory) + return true + }), ) .post( "/worktree/reset", @@ -324,11 +315,13 @@ export const ExperimentalRoutes = lazy(() => }, }), validator("json", Worktree.ResetInput), - async (c) => { - const body = c.req.valid("json") - await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.reset(body))) - return c.json(true) - }, + async (c) => + jsonRequest("ExperimentalRoutes.worktree.reset", c, function* () { + const body = c.req.valid("json") + const svc = yield* Worktree.Service + yield* svc.reset(body) + return true + }), ) .get( "/session", @@ -406,15 +399,10 @@ export const ExperimentalRoutes = lazy(() => }, }, }), - async (c) => { - return c.json( - await AppRuntime.runPromise( - Effect.gen(function* () { - const mcp = yield* MCP.Service - return yield* mcp.resources() - }), - ), - ) - }, + async (c) => + jsonRequest("ExperimentalRoutes.resource.list", c, function* () { + const mcp = yield* MCP.Service + return yield* mcp.resources() + }), ), ) diff --git a/packages/opencode/src/server/routes/instance/file.ts b/packages/opencode/src/server/routes/instance/file.ts index a82e5687d8..bbef679a85 100644 --- a/packages/opencode/src/server/routes/instance/file.ts +++ b/packages/opencode/src/server/routes/instance/file.ts @@ -1,13 +1,12 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" -import { Effect } from "effect" import z from "zod" -import { AppRuntime } from "@/effect/app-runtime" import { File } from "@/file" import { Ripgrep } from "@/file/ripgrep" import { LSP } from "@/lsp" import { Instance } from "@/project/instance" import { lazy } from "@/util/lazy" +import { jsonRequest } from "./trace" export const FileRoutes = lazy(() => new Hono() @@ -34,13 +33,13 @@ export const FileRoutes = lazy(() => pattern: z.string(), }), ), - async (c) => { - const pattern = c.req.valid("query").pattern - const result = await AppRuntime.runPromise( - Ripgrep.Service.use((svc) => svc.search({ cwd: Instance.directory, pattern, limit: 10 })), - ) - return c.json(result.items) - }, + async (c) => + jsonRequest("FileRoutes.findText", c, function* () { + const pattern = c.req.valid("query").pattern + const svc = yield* Ripgrep.Service + const result = yield* svc.search({ cwd: Instance.directory, pattern, limit: 10 }) + return result.items + }), ) .get( "/find/file", @@ -68,25 +67,17 @@ export const FileRoutes = lazy(() => limit: z.coerce.number().int().min(1).max(200).optional(), }), ), - async (c) => { - const query = c.req.valid("query").query - const dirs = c.req.valid("query").dirs - const type = c.req.valid("query").type - const limit = c.req.valid("query").limit - const results = await AppRuntime.runPromise( - Effect.gen(function* () { - return yield* File.Service.use((svc) => - svc.search({ - query, - limit: limit ?? 10, - dirs: dirs !== "false", - type, - }), - ) - }), - ) - return c.json(results) - }, + async (c) => + jsonRequest("FileRoutes.findFile", c, function* () { + const query = c.req.valid("query") + const svc = yield* File.Service + return yield* svc.search({ + query: query.query, + limit: query.limit ?? 10, + dirs: query.dirs !== "false", + type: query.type, + }) + }), ) .get( "/find/symbol", @@ -138,15 +129,11 @@ export const FileRoutes = lazy(() => path: z.string(), }), ), - async (c) => { - const path = c.req.valid("query").path - const content = await AppRuntime.runPromise( - Effect.gen(function* () { - return yield* File.Service.use((svc) => svc.list(path)) - }), - ) - return c.json(content) - }, + async (c) => + jsonRequest("FileRoutes.list", c, function* () { + const svc = yield* File.Service + return yield* svc.list(c.req.valid("query").path) + }), ) .get( "/file/content", @@ -171,15 +158,11 @@ export const FileRoutes = lazy(() => path: z.string(), }), ), - async (c) => { - const path = c.req.valid("query").path - const content = await AppRuntime.runPromise( - Effect.gen(function* () { - return yield* File.Service.use((svc) => svc.read(path)) - }), - ) - return c.json(content) - }, + async (c) => + jsonRequest("FileRoutes.read", c, function* () { + const svc = yield* File.Service + return yield* svc.read(c.req.valid("query").path) + }), ) .get( "/file/status", @@ -198,13 +181,10 @@ export const FileRoutes = lazy(() => }, }, }), - async (c) => { - const content = await AppRuntime.runPromise( - Effect.gen(function* () { - return yield* File.Service.use((svc) => svc.status()) - }), - ) - return c.json(content) - }, + async (c) => + jsonRequest("FileRoutes.status", c, function* () { + const svc = yield* File.Service + return yield* svc.status() + }), ), ) diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index ddb7e335ad..5cc51d27ab 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -26,8 +26,8 @@ import { ExperimentalRoutes } from "./experimental" import { ProviderRoutes } from "./provider" import { EventRoutes } from "./event" import { SyncRoutes } from "./sync" -import { AppRuntime } from "@/effect/app-runtime" import { InstanceMiddleware } from "./middleware" +import { jsonRequest } from "./trace" export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { const app = new Hono() @@ -141,19 +141,14 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { }, }, }), - async (c) => { - return c.json( - await AppRuntime.runPromise( - Effect.gen(function* () { - const vcs = yield* Vcs.Service - const [branch, default_branch] = yield* Effect.all([vcs.branch(), vcs.defaultBranch()], { - concurrency: 2, - }) - return { branch, default_branch } - }), - ), - ) - }, + async (c) => + jsonRequest("InstanceRoutes.vcs.get", c, function* () { + const vcs = yield* Vcs.Service + const [branch, default_branch] = yield* Effect.all([vcs.branch(), vcs.defaultBranch()], { + concurrency: 2, + }) + return { branch, default_branch } + }), ) .get( "/vcs/diff", @@ -178,16 +173,11 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { mode: Vcs.Mode, }), ), - async (c) => { - return c.json( - await AppRuntime.runPromise( - Effect.gen(function* () { - const vcs = yield* Vcs.Service - return yield* vcs.diff(c.req.valid("query").mode) - }), - ), - ) - }, + async (c) => + jsonRequest("InstanceRoutes.vcs.diff", c, function* () { + const vcs = yield* Vcs.Service + return yield* vcs.diff(c.req.valid("query").mode) + }), ) .get( "/command", @@ -206,10 +196,11 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { }, }, }), - async (c) => { - const commands = await AppRuntime.runPromise(Command.Service.use((svc) => svc.list())) - return c.json(commands) - }, + async (c) => + jsonRequest("InstanceRoutes.command.list", c, function* () { + const svc = yield* Command.Service + return yield* svc.list() + }), ) .get( "/agent", @@ -228,10 +219,11 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { }, }, }), - async (c) => { - const modes = await AppRuntime.runPromise(Agent.Service.use((svc) => svc.list())) - return c.json(modes) - }, + async (c) => + jsonRequest("InstanceRoutes.agent.list", c, function* () { + const svc = yield* Agent.Service + return yield* svc.list() + }), ) .get( "/skill", @@ -250,15 +242,11 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { }, }, }), - async (c) => { - const skills = await AppRuntime.runPromise( - Effect.gen(function* () { - const skill = yield* Skill.Service - return yield* skill.all() - }), - ) - return c.json(skills) - }, + async (c) => + jsonRequest("InstanceRoutes.skill.list", c, function* () { + const skill = yield* Skill.Service + return yield* skill.all() + }), ) .get( "/lsp", @@ -277,10 +265,11 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { }, }, }), - async (c) => { - const items = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.status())) - return c.json(items) - }, + async (c) => + jsonRequest("InstanceRoutes.lsp.status", c, function* () { + const lsp = yield* LSP.Service + return yield* lsp.status() + }), ) .get( "/formatter", @@ -299,8 +288,10 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { }, }, }), - async (c) => { - return c.json(await AppRuntime.runPromise(Format.Service.use((svc) => svc.status()))) - }, + async (c) => + jsonRequest("InstanceRoutes.formatter.status", c, function* () { + const svc = yield* Format.Service + return yield* svc.status() + }), ) } diff --git a/packages/opencode/src/server/routes/instance/mcp.ts b/packages/opencode/src/server/routes/instance/mcp.ts index b42cfb5314..ce4722933b 100644 --- a/packages/opencode/src/server/routes/instance/mcp.ts +++ b/packages/opencode/src/server/routes/instance/mcp.ts @@ -2,12 +2,11 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" import { MCP } from "@/mcp" -import { Config } from "@/config" import { ConfigMCP } from "@/config/mcp" -import { AppRuntime } from "@/effect/app-runtime" import { errors } from "../../error" import { lazy } from "@/util/lazy" import { Effect } from "effect" +import { jsonRequest, runRequest } from "./trace" export const McpRoutes = lazy(() => new Hono() @@ -28,9 +27,11 @@ export const McpRoutes = lazy(() => }, }, }), - async (c) => { - return c.json(await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.status()))) - }, + async (c) => + jsonRequest("McpRoutes.status", c, function* () { + const mcp = yield* MCP.Service + return yield* mcp.status() + }), ) .post( "/", @@ -57,11 +58,13 @@ export const McpRoutes = lazy(() => config: ConfigMCP.Info.zod, }), ), - async (c) => { - const { name, config } = c.req.valid("json") - const result = await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.add(name, config))) - return c.json(result.status) - }, + async (c) => + jsonRequest("McpRoutes.add", c, function* () { + const { name, config } = c.req.valid("json") + const mcp = yield* MCP.Service + const result = yield* mcp.add(name, config) + return result.status + }), ) .post( "/:name/auth", @@ -87,7 +90,9 @@ export const McpRoutes = lazy(() => }), async (c) => { const name = c.req.param("name") - const result = await AppRuntime.runPromise( + const result = await runRequest( + "McpRoutes.auth.start", + c, Effect.gen(function* () { const mcp = yield* MCP.Service const supports = yield* mcp.supportsOAuth(name) @@ -129,12 +134,13 @@ export const McpRoutes = lazy(() => code: z.string().describe("Authorization code from OAuth callback"), }), ), - async (c) => { - const name = c.req.param("name") - const { code } = c.req.valid("json") - const status = await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.finishAuth(name, code))) - return c.json(status) - }, + async (c) => + jsonRequest("McpRoutes.auth.callback", c, function* () { + const name = c.req.param("name") + const { code } = c.req.valid("json") + const mcp = yield* MCP.Service + return yield* mcp.finishAuth(name, code) + }), ) .post( "/:name/auth/authenticate", @@ -156,7 +162,9 @@ export const McpRoutes = lazy(() => }), async (c) => { const name = c.req.param("name") - const result = await AppRuntime.runPromise( + const result = await runRequest( + "McpRoutes.auth.authenticate", + c, Effect.gen(function* () { const mcp = yield* MCP.Service const supports = yield* mcp.supportsOAuth(name) @@ -191,11 +199,13 @@ export const McpRoutes = lazy(() => ...errors(404), }, }), - async (c) => { - const name = c.req.param("name") - await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.removeAuth(name))) - return c.json({ success: true as const }) - }, + async (c) => + jsonRequest("McpRoutes.auth.remove", c, function* () { + const name = c.req.param("name") + const mcp = yield* MCP.Service + yield* mcp.removeAuth(name) + return { success: true as const } + }), ) .post( "/:name/connect", @@ -214,11 +224,13 @@ export const McpRoutes = lazy(() => }, }), validator("param", z.object({ name: z.string() })), - async (c) => { - const { name } = c.req.valid("param") - await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.connect(name))) - return c.json(true) - }, + async (c) => + jsonRequest("McpRoutes.connect", c, function* () { + const { name } = c.req.valid("param") + const mcp = yield* MCP.Service + yield* mcp.connect(name) + return true + }), ) .post( "/:name/disconnect", @@ -237,10 +249,12 @@ export const McpRoutes = lazy(() => }, }), validator("param", z.object({ name: z.string() })), - async (c) => { - const { name } = c.req.valid("param") - await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.disconnect(name))) - return c.json(true) - }, + async (c) => + jsonRequest("McpRoutes.disconnect", c, function* () { + const { name } = c.req.valid("param") + const mcp = yield* MCP.Service + yield* mcp.disconnect(name) + return true + }), ), ) diff --git a/packages/opencode/src/server/routes/instance/permission.ts b/packages/opencode/src/server/routes/instance/permission.ts index c3f9c82011..c18f4734b4 100644 --- a/packages/opencode/src/server/routes/instance/permission.ts +++ b/packages/opencode/src/server/routes/instance/permission.ts @@ -1,11 +1,11 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" -import { AppRuntime } from "@/effect/app-runtime" import { Permission } from "@/permission" import { PermissionID } from "@/permission/schema" import { errors } from "../../error" import { lazy } from "@/util/lazy" +import { jsonRequest } from "./trace" export const PermissionRoutes = lazy(() => new Hono() @@ -34,20 +34,18 @@ export const PermissionRoutes = lazy(() => }), ), validator("json", z.object({ reply: Permission.Reply.zod, message: z.string().optional() })), - async (c) => { - const params = c.req.valid("param") - const json = c.req.valid("json") - await AppRuntime.runPromise( - Permission.Service.use((svc) => - svc.reply({ - requestID: params.requestID, - reply: json.reply, - message: json.message, - }), - ), - ) - return c.json(true) - }, + async (c) => + jsonRequest("PermissionRoutes.reply", c, function* () { + const params = c.req.valid("param") + const json = c.req.valid("json") + const svc = yield* Permission.Service + yield* svc.reply({ + requestID: params.requestID, + reply: json.reply, + message: json.message, + }) + return true + }), ) .get( "/", @@ -66,9 +64,10 @@ export const PermissionRoutes = lazy(() => }, }, }), - async (c) => { - const permissions = await AppRuntime.runPromise(Permission.Service.use((svc) => svc.list())) - return c.json(permissions) - }, + async (c) => + jsonRequest("PermissionRoutes.list", c, function* () { + const svc = yield* Permission.Service + return yield* svc.list() + }), ), ) diff --git a/packages/opencode/src/server/routes/instance/project.ts b/packages/opencode/src/server/routes/instance/project.ts index 060542c4b4..5acef6d788 100644 --- a/packages/opencode/src/server/routes/instance/project.ts +++ b/packages/opencode/src/server/routes/instance/project.ts @@ -9,6 +9,7 @@ import { errors } from "../../error" import { lazy } from "@/util/lazy" import { InstanceBootstrap } from "@/project/bootstrap" import { AppRuntime } from "@/effect/app-runtime" +import { jsonRequest, runRequest } from "./trace" export const ProjectRoutes = lazy(() => new Hono() @@ -75,7 +76,9 @@ export const ProjectRoutes = lazy(() => async (c) => { const dir = Instance.directory const prev = Instance.project - const next = await AppRuntime.runPromise( + const next = await runRequest( + "ProjectRoutes.initGit", + c, Project.Service.use((svc) => svc.initGit({ directory: dir, project: prev })), ) if (next.id === prev.id && next.vcs === prev.vcs && next.worktree === prev.worktree) return c.json(next) @@ -108,11 +111,12 @@ export const ProjectRoutes = lazy(() => }), validator("param", z.object({ projectID: ProjectID.zod })), validator("json", Project.UpdateInput.omit({ projectID: true })), - async (c) => { - const projectID = c.req.valid("param").projectID - const body = c.req.valid("json") - const project = await AppRuntime.runPromise(Project.Service.use((svc) => svc.update({ ...body, projectID }))) - return c.json(project) - }, + async (c) => + jsonRequest("ProjectRoutes.update", c, function* () { + const projectID = c.req.valid("param").projectID + const body = c.req.valid("json") + const svc = yield* Project.Service + return yield* svc.update({ ...body, projectID }) + }), ), ) diff --git a/packages/opencode/src/server/routes/instance/provider.ts b/packages/opencode/src/server/routes/instance/provider.ts index 57aa895e3d..617980e39c 100644 --- a/packages/opencode/src/server/routes/instance/provider.ts +++ b/packages/opencode/src/server/routes/instance/provider.ts @@ -6,11 +6,11 @@ import { Provider } from "@/provider" import { ModelsDev } from "@/provider" import { ProviderAuth } from "@/provider" import { ProviderID } from "@/provider/schema" -import { AppRuntime } from "@/effect/app-runtime" import { mapValues } from "remeda" import { errors } from "../../error" import { lazy } from "@/util/lazy" import { Effect } from "effect" +import { jsonRequest } from "./trace" export const ProviderRoutes = lazy(() => new Hono() @@ -31,39 +31,31 @@ export const ProviderRoutes = lazy(() => }, }, }), - async (c) => { - const result = await AppRuntime.runPromise( - Effect.gen(function* () { - const svc = yield* Provider.Service - const cfg = yield* Config.Service - const config = yield* cfg.get() - const all = yield* Effect.promise(() => ModelsDev.get()) - const disabled = new Set(config.disabled_providers ?? []) - const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined - const filtered: Record = {} - for (const [key, value] of Object.entries(all)) { - if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) { - filtered[key] = value - } + async (c) => + jsonRequest("ProviderRoutes.list", c, function* () { + const svc = yield* Provider.Service + const cfg = yield* Config.Service + const config = yield* cfg.get() + const all = yield* Effect.promise(() => ModelsDev.get()) + const disabled = new Set(config.disabled_providers ?? []) + const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined + const filtered: Record = {} + for (const [key, value] of Object.entries(all)) { + if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) { + filtered[key] = value } - const connected = yield* svc.list() - const providers = Object.assign( - mapValues(filtered, (x) => Provider.fromModelsDevProvider(x)), - connected, - ) - return { - all: Object.values(providers), - default: Provider.defaultModelIDs(providers), - connected: Object.keys(connected), - } - }), - ) - return c.json({ - all: result.all, - default: result.default, - connected: result.connected, - }) - }, + } + const connected = yield* svc.list() + const providers = Object.assign( + mapValues(filtered, (x) => Provider.fromModelsDevProvider(x)), + connected, + ) + return { + all: Object.values(providers), + default: Provider.defaultModelIDs(providers), + connected: Object.keys(connected), + } + }), ) .get( "/auth", @@ -82,9 +74,11 @@ export const ProviderRoutes = lazy(() => }, }, }), - async (c) => { - return c.json(await AppRuntime.runPromise(ProviderAuth.Service.use((svc) => svc.methods()))) - }, + async (c) => + jsonRequest("ProviderRoutes.auth", c, function* () { + const svc = yield* ProviderAuth.Service + return yield* svc.methods() + }), ) .post( "/:providerID/oauth/authorize", @@ -111,20 +105,17 @@ export const ProviderRoutes = lazy(() => }), ), validator("json", ProviderAuth.AuthorizeInput.zod), - async (c) => { - const providerID = c.req.valid("param").providerID - const { method, inputs } = c.req.valid("json") - const result = await AppRuntime.runPromise( - ProviderAuth.Service.use((svc) => - svc.authorize({ - providerID, - method, - inputs, - }), - ), - ) - return c.json(result) - }, + async (c) => + jsonRequest("ProviderRoutes.oauth.authorize", c, function* () { + const providerID = c.req.valid("param").providerID + const { method, inputs } = c.req.valid("json") + const svc = yield* ProviderAuth.Service + return yield* svc.authorize({ + providerID, + method, + inputs, + }) + }), ) .post( "/:providerID/oauth/callback", @@ -151,19 +142,17 @@ export const ProviderRoutes = lazy(() => }), ), validator("json", ProviderAuth.CallbackInput.zod), - async (c) => { - const providerID = c.req.valid("param").providerID - const { method, code } = c.req.valid("json") - await AppRuntime.runPromise( - ProviderAuth.Service.use((svc) => - svc.callback({ - providerID, - method, - code, - }), - ), - ) - return c.json(true) - }, + async (c) => + jsonRequest("ProviderRoutes.oauth.callback", c, function* () { + const providerID = c.req.valid("param").providerID + const { method, code } = c.req.valid("json") + const svc = yield* ProviderAuth.Service + yield* svc.callback({ + providerID, + method, + code, + }) + return true + }), ), ) diff --git a/packages/opencode/src/server/routes/instance/pty.ts b/packages/opencode/src/server/routes/instance/pty.ts index b3f71c235c..a25b66e9ff 100644 --- a/packages/opencode/src/server/routes/instance/pty.ts +++ b/packages/opencode/src/server/routes/instance/pty.ts @@ -8,6 +8,7 @@ import { Pty } from "@/pty" import { PtyID } from "@/pty/schema" import { NotFoundError } from "@/storage" import { errors } from "../../error" +import { jsonRequest, runRequest } from "./trace" export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) { return new Hono() @@ -28,16 +29,11 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) { }, }, }), - async (c) => { - return c.json( - await AppRuntime.runPromise( - Effect.gen(function* () { - const pty = yield* Pty.Service - return yield* pty.list() - }), - ), - ) - }, + async (c) => + jsonRequest("PtyRoutes.list", c, function* () { + const pty = yield* Pty.Service + return yield* pty.list() + }), ) .post( "/", @@ -58,15 +54,11 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) { }, }), validator("json", Pty.CreateInput), - async (c) => { - const info = await AppRuntime.runPromise( - Effect.gen(function* () { - const pty = yield* Pty.Service - return yield* pty.create(c.req.valid("json")) - }), - ) - return c.json(info) - }, + async (c) => + jsonRequest("PtyRoutes.create", c, function* () { + const pty = yield* Pty.Service + return yield* pty.create(c.req.valid("json")) + }), ) .get( "/:ptyID", @@ -88,7 +80,9 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) { }), validator("param", z.object({ ptyID: PtyID.zod })), async (c) => { - const info = await AppRuntime.runPromise( + const info = await runRequest( + "PtyRoutes.get", + c, Effect.gen(function* () { const pty = yield* Pty.Service return yield* pty.get(c.req.valid("param").ptyID) @@ -120,15 +114,11 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) { }), validator("param", z.object({ ptyID: PtyID.zod })), validator("json", Pty.UpdateInput), - async (c) => { - const info = await AppRuntime.runPromise( - Effect.gen(function* () { - const pty = yield* Pty.Service - return yield* pty.update(c.req.valid("param").ptyID, c.req.valid("json")) - }), - ) - return c.json(info) - }, + async (c) => + jsonRequest("PtyRoutes.update", c, function* () { + const pty = yield* Pty.Service + return yield* pty.update(c.req.valid("param").ptyID, c.req.valid("json")) + }), ) .delete( "/:ptyID", @@ -149,15 +139,12 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) { }, }), validator("param", z.object({ ptyID: PtyID.zod })), - async (c) => { - await AppRuntime.runPromise( - Effect.gen(function* () { - const pty = yield* Pty.Service - yield* pty.remove(c.req.valid("param").ptyID) - }), - ) - return c.json(true) - }, + async (c) => + jsonRequest("PtyRoutes.remove", c, function* () { + const pty = yield* Pty.Service + yield* pty.remove(c.req.valid("param").ptyID) + return true + }), ) .get( "/:ptyID/connect", @@ -194,7 +181,9 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) { })() let handler: Handler | undefined if ( - !(await AppRuntime.runPromise( + !(await runRequest( + "PtyRoutes.connect", + c, Effect.gen(function* () { const pty = yield* Pty.Service return yield* pty.get(id) @@ -232,7 +221,7 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) { Effect.gen(function* () { const pty = yield* Pty.Service return yield* pty.connect(id, socket, cursor) - }), + }).pipe(Effect.withSpan("PtyRoutes.connect.open")), ) ready = true for (const msg of pending) handler?.onMessage(msg) diff --git a/packages/opencode/src/server/routes/instance/question.ts b/packages/opencode/src/server/routes/instance/question.ts index 9b8f461e39..51ecb48ccd 100644 --- a/packages/opencode/src/server/routes/instance/question.ts +++ b/packages/opencode/src/server/routes/instance/question.ts @@ -3,10 +3,10 @@ import { describeRoute, validator } from "hono-openapi" import { resolver } from "hono-openapi" import { QuestionID } from "@/question/schema" import { Question } from "@/question" -import { AppRuntime } from "@/effect/app-runtime" import z from "zod" import { errors } from "../../error" import { lazy } from "@/util/lazy" +import { jsonRequest } from "./trace" const Reply = z.object({ answers: Question.Answer.zod @@ -33,10 +33,11 @@ export const QuestionRoutes = lazy(() => }, }, }), - async (c) => { - const questions = await AppRuntime.runPromise(Question.Service.use((svc) => svc.list())) - return c.json(questions) - }, + async (c) => + jsonRequest("QuestionRoutes.list", c, function* () { + const svc = yield* Question.Service + return yield* svc.list() + }), ) .post( "/:requestID/reply", @@ -63,19 +64,17 @@ export const QuestionRoutes = lazy(() => }), ), validator("json", Reply), - async (c) => { - const params = c.req.valid("param") - const json = c.req.valid("json") - await AppRuntime.runPromise( - Question.Service.use((svc) => - svc.reply({ - requestID: params.requestID, - answers: json.answers, - }), - ), - ) - return c.json(true) - }, + async (c) => + jsonRequest("QuestionRoutes.reply", c, function* () { + const params = c.req.valid("param") + const json = c.req.valid("json") + const svc = yield* Question.Service + yield* svc.reply({ + requestID: params.requestID, + answers: json.answers, + }) + return true + }), ) .post( "/:requestID/reject", @@ -101,10 +100,12 @@ export const QuestionRoutes = lazy(() => requestID: QuestionID.zod, }), ), - async (c) => { - const params = c.req.valid("param") - await AppRuntime.runPromise(Question.Service.use((svc) => svc.reject(params.requestID))) - return c.json(true) - }, + async (c) => + jsonRequest("QuestionRoutes.reject", c, function* () { + const params = c.req.valid("param") + const svc = yield* Question.Service + yield* svc.reject(params.requestID) + return true + }), ), ) diff --git a/packages/opencode/src/server/routes/instance/session.ts b/packages/opencode/src/server/routes/instance/session.ts index ae6185abb8..bf713935b0 100644 --- a/packages/opencode/src/server/routes/instance/session.ts +++ b/packages/opencode/src/server/routes/instance/session.ts @@ -14,7 +14,6 @@ import { SessionStatus } from "@/session/status" import { SessionSummary } from "@/session/summary" import { Todo } from "@/session/todo" import { Effect } from "effect" -import { AppRuntime } from "@/effect/app-runtime" import { Agent } from "@/agent/agent" import { Snapshot } from "@/snapshot" import { Command } from "@/command" @@ -26,7 +25,7 @@ import { errors } from "../../error" import { lazy } from "@/util/lazy" import { Bus } from "@/bus" import { NamedError } from "@opencode-ai/shared/util/error" -import { jsonRequest } from "./trace" +import { jsonRequest, runRequest } from "./trace" const log = Log.create({ service: "server" }) @@ -218,11 +217,12 @@ export const SessionRoutes = lazy(() => }, }), validator("json", Session.CreateInput), - async (c) => { - const body = c.req.valid("json") ?? {} - const session = await AppRuntime.runPromise(SessionShare.Service.use((svc) => svc.create(body))) - return c.json(session) - }, + async (c) => + jsonRequest("SessionRoutes.create", c, function* () { + const body = c.req.valid("json") ?? {} + const svc = yield* SessionShare.Service + return yield* svc.create(body) + }), ) .delete( "/:sessionID", @@ -248,11 +248,13 @@ export const SessionRoutes = lazy(() => sessionID: Session.RemoveInput, }), ), - async (c) => { - const sessionID = c.req.valid("param").sessionID - await AppRuntime.runPromise(Session.Service.use((svc) => svc.remove(sessionID))) - return c.json(true) - }, + async (c) => + jsonRequest("SessionRoutes.delete", c, function* () { + const sessionID = c.req.valid("param").sessionID + const svc = yield* Session.Service + yield* svc.remove(sessionID) + return true + }), ) .patch( "/:sessionID", @@ -290,32 +292,28 @@ export const SessionRoutes = lazy(() => .optional(), }), ), - async (c) => { - const sessionID = c.req.valid("param").sessionID - const updates = c.req.valid("json") - const session = await AppRuntime.runPromise( - Effect.gen(function* () { - const session = yield* Session.Service - const current = yield* session.get(sessionID) + async (c) => + jsonRequest("SessionRoutes.update", c, function* () { + const sessionID = c.req.valid("param").sessionID + const updates = c.req.valid("json") + const session = yield* Session.Service + const current = yield* session.get(sessionID) - if (updates.title !== undefined) { - yield* session.setTitle({ sessionID, title: updates.title }) - } - if (updates.permission !== undefined) { - yield* session.setPermission({ - sessionID, - permission: Permission.merge(current.permission ?? [], updates.permission), - }) - } - if (updates.time?.archived !== undefined) { - yield* session.setArchived({ sessionID, time: updates.time.archived }) - } + if (updates.title !== undefined) { + yield* session.setTitle({ sessionID, title: updates.title }) + } + if (updates.permission !== undefined) { + yield* session.setPermission({ + sessionID, + permission: Permission.merge(current.permission ?? [], updates.permission), + }) + } + if (updates.time?.archived !== undefined) { + yield* session.setArchived({ sessionID, time: updates.time.archived }) + } - return yield* session.get(sessionID) - }), - ) - return c.json(session) - }, + return yield* session.get(sessionID) + }), ) // TODO(v2): remove this dedicated route and rely on the normal `/init` command flow. .post( @@ -351,22 +349,20 @@ export const SessionRoutes = lazy(() => messageID: MessageID.zod, }), ), - async (c) => { - const sessionID = c.req.valid("param").sessionID - const body = c.req.valid("json") - await AppRuntime.runPromise( - SessionPrompt.Service.use((svc) => - svc.command({ - sessionID, - messageID: body.messageID, - model: body.providerID + "/" + body.modelID, - command: Command.Default.INIT, - arguments: "", - }), - ), - ) - return c.json(true) - }, + async (c) => + jsonRequest("SessionRoutes.init", c, function* () { + const sessionID = c.req.valid("param").sessionID + const body = c.req.valid("json") + const svc = yield* SessionPrompt.Service + yield* svc.command({ + sessionID, + messageID: body.messageID, + model: body.providerID + "/" + body.modelID, + command: Command.Default.INIT, + arguments: "", + }) + return true + }), ) .post( "/:sessionID/fork", @@ -392,12 +388,13 @@ export const SessionRoutes = lazy(() => }), ), validator("json", Session.ForkInput.omit({ sessionID: true })), - async (c) => { - const sessionID = c.req.valid("param").sessionID - const body = c.req.valid("json") - const result = await AppRuntime.runPromise(Session.Service.use((svc) => svc.fork({ ...body, sessionID }))) - return c.json(result) - }, + async (c) => + jsonRequest("SessionRoutes.fork", c, function* () { + const sessionID = c.req.valid("param").sessionID + const body = c.req.valid("json") + const svc = yield* Session.Service + return yield* svc.fork({ ...body, sessionID }) + }), ) .post( "/:sessionID/abort", @@ -423,10 +420,12 @@ export const SessionRoutes = lazy(() => sessionID: SessionID.zod, }), ), - async (c) => { - await AppRuntime.runPromise(SessionPrompt.Service.use((svc) => svc.cancel(c.req.valid("param").sessionID))) - return c.json(true) - }, + async (c) => + jsonRequest("SessionRoutes.abort", c, function* () { + const svc = yield* SessionPrompt.Service + yield* svc.cancel(c.req.valid("param").sessionID) + return true + }), ) .post( "/:sessionID/share", @@ -452,18 +451,14 @@ export const SessionRoutes = lazy(() => sessionID: SessionID.zod, }), ), - async (c) => { - const sessionID = c.req.valid("param").sessionID - const session = await AppRuntime.runPromise( - Effect.gen(function* () { - const share = yield* SessionShare.Service - const session = yield* Session.Service - yield* share.share(sessionID) - return yield* session.get(sessionID) - }), - ) - return c.json(session) - }, + async (c) => + jsonRequest("SessionRoutes.share", c, function* () { + const sessionID = c.req.valid("param").sessionID + const share = yield* SessionShare.Service + const session = yield* Session.Service + yield* share.share(sessionID) + return yield* session.get(sessionID) + }), ) .get( "/:sessionID/diff", @@ -494,19 +489,16 @@ export const SessionRoutes = lazy(() => messageID: SessionSummary.DiffInput.shape.messageID, }), ), - async (c) => { - const query = c.req.valid("query") - const params = c.req.valid("param") - const result = await AppRuntime.runPromise( - SessionSummary.Service.use((summary) => - summary.diff({ - sessionID: params.sessionID, - messageID: query.messageID, - }), - ), - ) - return c.json(result) - }, + async (c) => + jsonRequest("SessionRoutes.diff", c, function* () { + const query = c.req.valid("query") + const params = c.req.valid("param") + const summary = yield* SessionSummary.Service + return yield* summary.diff({ + sessionID: params.sessionID, + messageID: query.messageID, + }) + }), ) .delete( "/:sessionID/share", @@ -532,18 +524,14 @@ export const SessionRoutes = lazy(() => sessionID: SessionID.zod, }), ), - async (c) => { - const sessionID = c.req.valid("param").sessionID - const session = await AppRuntime.runPromise( - Effect.gen(function* () { - const share = yield* SessionShare.Service - const session = yield* Session.Service - yield* share.unshare(sessionID) - return yield* session.get(sessionID) - }), - ) - return c.json(session) - }, + async (c) => + jsonRequest("SessionRoutes.unshare", c, function* () { + const sessionID = c.req.valid("param").sessionID + const share = yield* SessionShare.Service + const session = yield* Session.Service + yield* share.unshare(sessionID) + return yield* session.get(sessionID) + }), ) .post( "/:sessionID/summarize", @@ -577,43 +565,40 @@ export const SessionRoutes = lazy(() => auto: z.boolean().optional().default(false), }), ), - async (c) => { - const sessionID = c.req.valid("param").sessionID - const body = c.req.valid("json") - await AppRuntime.runPromise( - Effect.gen(function* () { - const session = yield* Session.Service - const revert = yield* SessionRevert.Service - const compact = yield* SessionCompaction.Service - const prompt = yield* SessionPrompt.Service - const agent = yield* Agent.Service + async (c) => + jsonRequest("SessionRoutes.summarize", c, function* () { + const sessionID = c.req.valid("param").sessionID + const body = c.req.valid("json") + const session = yield* Session.Service + const revert = yield* SessionRevert.Service + const compact = yield* SessionCompaction.Service + const prompt = yield* SessionPrompt.Service + const agent = yield* Agent.Service - yield* revert.cleanup(yield* session.get(sessionID)) - const msgs = yield* session.messages({ sessionID }) - const defaultAgent = yield* agent.defaultAgent() - let currentAgent = defaultAgent - for (let i = msgs.length - 1; i >= 0; i--) { - const info = msgs[i].info - if (info.role === "user") { - currentAgent = info.agent || defaultAgent - break - } + yield* revert.cleanup(yield* session.get(sessionID)) + const msgs = yield* session.messages({ sessionID }) + const defaultAgent = yield* agent.defaultAgent() + let currentAgent = defaultAgent + for (let i = msgs.length - 1; i >= 0; i--) { + const info = msgs[i].info + if (info.role === "user") { + currentAgent = info.agent || defaultAgent + break } + } - yield* compact.create({ - sessionID, - agent: currentAgent, - model: { - providerID: body.providerID, - modelID: body.modelID, - }, - auto: body.auto, - }) - yield* prompt.loop({ sessionID }) - }), - ) - return c.json(true) - }, + yield* compact.create({ + sessionID, + agent: currentAgent, + model: { + providerID: body.providerID, + modelID: body.modelID, + }, + auto: body.auto, + }) + yield* prompt.loop({ sessionID }) + return true + }), ) .get( "/:sessionID/message", @@ -675,7 +660,9 @@ export const SessionRoutes = lazy(() => const query = c.req.valid("query") const sessionID = c.req.valid("param").sessionID if (query.limit === undefined || query.limit === 0) { - const messages = await AppRuntime.runPromise( + const messages = await runRequest( + "SessionRoutes.messages", + c, Effect.gen(function* () { const session = yield* Session.Service yield* session.get(sessionID) @@ -766,21 +753,18 @@ export const SessionRoutes = lazy(() => messageID: MessageID.zod, }), ), - async (c) => { - const params = c.req.valid("param") - await AppRuntime.runPromise( - Effect.gen(function* () { - const state = yield* SessionRunState.Service - const session = yield* Session.Service - yield* state.assertNotBusy(params.sessionID) - yield* session.removeMessage({ - sessionID: params.sessionID, - messageID: params.messageID, - }) - }), - ) - return c.json(true) - }, + async (c) => + jsonRequest("SessionRoutes.deleteMessage", c, function* () { + const params = c.req.valid("param") + const state = yield* SessionRunState.Service + const session = yield* Session.Service + yield* state.assertNotBusy(params.sessionID) + yield* session.removeMessage({ + sessionID: params.sessionID, + messageID: params.messageID, + }) + return true + }), ) .delete( "/:sessionID/message/:messageID/part/:partID", @@ -807,19 +791,17 @@ export const SessionRoutes = lazy(() => partID: PartID.zod, }), ), - async (c) => { - const params = c.req.valid("param") - await AppRuntime.runPromise( - Session.Service.use((svc) => - svc.removePart({ - sessionID: params.sessionID, - messageID: params.messageID, - partID: params.partID, - }), - ), - ) - return c.json(true) - }, + async (c) => + jsonRequest("SessionRoutes.deletePart", c, function* () { + const params = c.req.valid("param") + const svc = yield* Session.Service + yield* svc.removePart({ + sessionID: params.sessionID, + messageID: params.messageID, + partID: params.partID, + }) + return true + }), ) .patch( "/:sessionID/message/:messageID/part/:partID", @@ -855,8 +837,10 @@ export const SessionRoutes = lazy(() => `Part mismatch: body.id='${body.id}' vs partID='${params.partID}', body.messageID='${body.messageID}' vs messageID='${params.messageID}', body.sessionID='${body.sessionID}' vs sessionID='${params.sessionID}'`, ) } - const part = await AppRuntime.runPromise(Session.Service.use((svc) => svc.updatePart(body))) - return c.json(part) + return jsonRequest("SessionRoutes.updatePart", c, function* () { + const svc = yield* Session.Service + return yield* svc.updatePart(body) + }) }, ) .post( @@ -895,7 +879,9 @@ export const SessionRoutes = lazy(() => return stream(c, async (stream) => { const sessionID = c.req.valid("param").sessionID const body = c.req.valid("json") - const msg = await AppRuntime.runPromise( + const msg = await runRequest( + "SessionRoutes.prompt", + c, SessionPrompt.Service.use((svc) => svc.prompt({ ...body, sessionID })), ) void stream.write(JSON.stringify(msg)) @@ -926,15 +912,17 @@ export const SessionRoutes = lazy(() => async (c) => { const sessionID = c.req.valid("param").sessionID const body = c.req.valid("json") - void AppRuntime.runPromise(SessionPrompt.Service.use((svc) => svc.prompt({ ...body, sessionID }))).catch( - (err) => { - log.error("prompt_async failed", { sessionID, error: err }) - void Bus.publish(Session.Event.Error, { - sessionID, - error: new NamedError.Unknown({ message: err instanceof Error ? err.message : String(err) }).toObject(), - }) - }, - ) + void runRequest( + "SessionRoutes.prompt_async", + c, + SessionPrompt.Service.use((svc) => svc.prompt({ ...body, sessionID })), + ).catch((err) => { + log.error("prompt_async failed", { sessionID, error: err }) + void Bus.publish(Session.Event.Error, { + sessionID, + error: new NamedError.Unknown({ message: err instanceof Error ? err.message : String(err) }).toObject(), + }) + }) return c.body(null, 204) }, @@ -969,12 +957,13 @@ export const SessionRoutes = lazy(() => }), ), validator("json", SessionPrompt.CommandInput.omit({ sessionID: true })), - async (c) => { - const sessionID = c.req.valid("param").sessionID - const body = c.req.valid("json") - const msg = await AppRuntime.runPromise(SessionPrompt.Service.use((svc) => svc.command({ ...body, sessionID }))) - return c.json(msg) - }, + async (c) => + jsonRequest("SessionRoutes.command", c, function* () { + const sessionID = c.req.valid("param").sessionID + const body = c.req.valid("json") + const svc = yield* SessionPrompt.Service + return yield* svc.command({ ...body, sessionID }) + }), ) .post( "/:sessionID/shell", @@ -1001,12 +990,13 @@ export const SessionRoutes = lazy(() => }), ), validator("json", SessionPrompt.ShellInput.omit({ sessionID: true })), - async (c) => { - const sessionID = c.req.valid("param").sessionID - const body = c.req.valid("json") - const msg = await AppRuntime.runPromise(SessionPrompt.Service.use((svc) => svc.shell({ ...body, sessionID }))) - return c.json(msg) - }, + async (c) => + jsonRequest("SessionRoutes.shell", c, function* () { + const sessionID = c.req.valid("param").sessionID + const body = c.req.valid("json") + const svc = yield* SessionPrompt.Service + return yield* svc.shell({ ...body, sessionID }) + }), ) .post( "/:sessionID/revert", @@ -1036,15 +1026,13 @@ export const SessionRoutes = lazy(() => async (c) => { const sessionID = c.req.valid("param").sessionID log.info("revert", c.req.valid("json")) - const session = await AppRuntime.runPromise( - SessionRevert.Service.use((svc) => - svc.revert({ - sessionID, - ...c.req.valid("json"), - }), - ), - ) - return c.json(session) + return jsonRequest("SessionRoutes.revert", c, function* () { + const svc = yield* SessionRevert.Service + return yield* svc.revert({ + sessionID, + ...c.req.valid("json"), + }) + }) }, ) .post( @@ -1071,11 +1059,12 @@ export const SessionRoutes = lazy(() => sessionID: SessionID.zod, }), ), - async (c) => { - const sessionID = c.req.valid("param").sessionID - const session = await AppRuntime.runPromise(SessionRevert.Service.use((svc) => svc.unrevert({ sessionID }))) - return c.json(session) - }, + async (c) => + jsonRequest("SessionRoutes.unrevert", c, function* () { + const sessionID = c.req.valid("param").sessionID + const svc = yield* SessionRevert.Service + return yield* svc.unrevert({ sessionID }) + }), ) .post( "/:sessionID/permissions/:permissionID", @@ -1104,17 +1093,15 @@ export const SessionRoutes = lazy(() => }), ), validator("json", z.object({ response: Permission.Reply.zod })), - async (c) => { - const params = c.req.valid("param") - await AppRuntime.runPromise( - Permission.Service.use((svc) => - svc.reply({ - requestID: params.permissionID, - reply: c.req.valid("json").response, - }), - ), - ) - return c.json(true) - }, + async (c) => + jsonRequest("SessionRoutes.permissionRespond", c, function* () { + const params = c.req.valid("param") + const svc = yield* Permission.Service + yield* svc.reply({ + requestID: params.permissionID, + reply: c.req.valid("json").response, + }) + return true + }), ), ) diff --git a/packages/opencode/src/server/routes/instance/tui.ts b/packages/opencode/src/server/routes/instance/tui.ts index 2f856c3488..d6add67b97 100644 --- a/packages/opencode/src/server/routes/instance/tui.ts +++ b/packages/opencode/src/server/routes/instance/tui.ts @@ -4,10 +4,10 @@ import z from "zod" import { Bus } from "@/bus" import { Session } from "@/session" import { TuiEvent } from "@/cli/cmd/tui/event" -import { AppRuntime } from "@/effect/app-runtime" import { AsyncQueue } from "@/util/queue" import { errors } from "../../error" import { lazy } from "@/util/lazy" +import { runRequest } from "./trace" const TuiRequest = z.object({ path: z.string(), @@ -371,7 +371,11 @@ export const TuiRoutes = lazy(() => validator("json", TuiEvent.SessionSelect.properties), async (c) => { const { sessionID } = c.req.valid("json") - await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID))) + await runRequest( + "TuiRoutes.sessionSelect", + c, + Session.Service.use((svc) => svc.get(sessionID)), + ) await Bus.publish(TuiEvent.SessionSelect, { sessionID }) return c.json(true) }, diff --git a/packages/opencode/src/server/workspace.ts b/packages/opencode/src/server/workspace.ts index c141d10956..d30a117d6a 100644 --- a/packages/opencode/src/server/workspace.ts +++ b/packages/opencode/src/server/workspace.ts @@ -10,6 +10,7 @@ import { Instance } from "@/project/instance" import { Session } from "@/session" import { SessionID } from "@/session/schema" import { AppRuntime } from "@/effect/app-runtime" +import { Effect } from "effect" import { Log } from "@/util" import { ServerProxy } from "./proxy" @@ -42,7 +43,9 @@ async function getSessionWorkspace(url: URL) { const id = getSessionID(url) if (!id) return null - const session = await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(id))).catch(() => undefined) + const session = await AppRuntime.runPromise( + Session.Service.use((svc) => svc.get(id)).pipe(Effect.withSpan("WorkspaceRouter.lookup")), + ).catch(() => undefined) return session?.workspaceID } From ce69bd97b90200a4d9794bd0409d3b4bb7996e54 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 16:59:24 -0400 Subject: [PATCH 138/335] refactor(config): migrate model-id and command to Effect Schema (#23175) --- packages/opencode/src/config/agent.ts | 2 +- packages/opencode/src/config/command.ts | 22 ++++++++++++---------- packages/opencode/src/config/config.ts | 8 +++++--- packages/opencode/src/config/model-id.ts | 13 ++++++++++++- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/config/agent.ts b/packages/opencode/src/config/agent.ts index f754f009d4..9053b19fc1 100644 --- a/packages/opencode/src/config/agent.ts +++ b/packages/opencode/src/config/agent.ts @@ -15,7 +15,7 @@ const log = Log.create({ service: "config" }) export const Info = z .object({ - model: ConfigModelID.optional(), + model: ConfigModelID.zod.optional(), variant: z .string() .optional() diff --git a/packages/opencode/src/config/command.ts b/packages/opencode/src/config/command.ts index 9799250567..3e0adccc30 100644 --- a/packages/opencode/src/config/command.ts +++ b/packages/opencode/src/config/command.ts @@ -1,10 +1,12 @@ export * as ConfigCommand from "./command" import { Log } from "../util" -import z from "zod" +import { Schema } from "effect" import { NamedError } from "@opencode-ai/shared/util/error" import { Glob } from "@opencode-ai/shared/util/glob" import { Bus } from "@/bus" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" import { configEntryNameFromPath } from "./entry-name" import { InvalidError } from "./error" import * as ConfigMarkdown from "./markdown" @@ -12,15 +14,15 @@ import { ConfigModelID } from "./model-id" const log = Log.create({ service: "config" }) -export const Info = z.object({ - template: z.string(), - description: z.string().optional(), - agent: z.string().optional(), - model: ConfigModelID.optional(), - subtask: z.boolean().optional(), -}) +export const Info = Schema.Struct({ + template: Schema.String, + description: Schema.optional(Schema.String), + agent: Schema.optional(Schema.String), + model: Schema.optional(ConfigModelID), + subtask: Schema.optional(Schema.Boolean), +}).pipe(withStatics((s) => ({ zod: zod(s) }))) -export type Info = z.infer +export type Info = Schema.Schema.Type export async function load(dir: string) { const result: Record = {} @@ -49,7 +51,7 @@ export async function load(dir: string) { ...md.data, template: md.content.trim(), } - const parsed = Info.safeParse(config) + const parsed = Info.zod.safeParse(config) if (parsed.success) { result[config.name] = parsed.data continue diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 261c5acab4..684fdf63ab 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -97,7 +97,7 @@ export const Info = z logLevel: Log.Level.optional().describe("Log level"), server: Server.optional().describe("Server configuration for opencode serve and web commands"), command: z - .record(z.string(), ConfigCommand.Info) + .record(z.string(), ConfigCommand.Info.zod) .optional() .describe("Command configuration, see https://opencode.ai/docs/commands"), skills: ConfigSkills.Info.zod.optional().describe("Additional skill folder paths"), @@ -135,8 +135,10 @@ export const Info = z .array(z.string()) .optional() .describe("When set, ONLY these providers will be enabled. All other providers will be ignored"), - model: ConfigModelID.describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(), - small_model: ConfigModelID.describe( + model: ConfigModelID.zod + .describe("Model to use in the format of provider/model, eg anthropic/claude-2") + .optional(), + small_model: ConfigModelID.zod.describe( "Small model to use for tasks like title generation in the format of provider/model", ).optional(), default_agent: z diff --git a/packages/opencode/src/config/model-id.ts b/packages/opencode/src/config/model-id.ts index 909e9aa929..3ad9e035ce 100644 --- a/packages/opencode/src/config/model-id.ts +++ b/packages/opencode/src/config/model-id.ts @@ -1,3 +1,14 @@ +import { Schema } from "effect" import z from "zod" +import { zod, ZodOverride } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" -export const ConfigModelID = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" }) +// The original Zod schema carried an external $ref pointing at the models.dev +// JSON schema. That external reference is not a named SDK component — it is a +// literal pointer to an outside schema — so the walker cannot re-derive it +// from AST metadata. Preserve the exact original Zod via ZodOverride. +export const ConfigModelID = Schema.String.annotate({ + [ZodOverride]: z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" }), +}).pipe(withStatics((s) => ({ zod: zod(s) }))) + +export type ConfigModelID = Schema.Schema.Type From 89029a20ef1548f6637c15f63f39f281e4a6dae7 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 21:00:20 +0000 Subject: [PATCH 139/335] chore: generate --- packages/opencode/src/config/config.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 684fdf63ab..f228878d04 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -135,12 +135,10 @@ export const Info = z .array(z.string()) .optional() .describe("When set, ONLY these providers will be enabled. All other providers will be ignored"), - model: ConfigModelID.zod - .describe("Model to use in the format of provider/model, eg anthropic/claude-2") + model: ConfigModelID.zod.describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(), + small_model: ConfigModelID.zod + .describe("Small model to use for tasks like title generation in the format of provider/model") .optional(), - small_model: ConfigModelID.zod.describe( - "Small model to use for tasks like title generation in the format of provider/model", - ).optional(), default_agent: z .string() .optional() From 5980b0a5eeb8f7a8dc31433f86e458dbe3358269 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 17:06:55 -0400 Subject: [PATCH 140/335] feat(effect-zod): add tuple support; migrate config/plugin to Effect Schema (#23178) --- .../src/cli/cmd/tui/config/tui-schema.ts | 2 +- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/config/plugin.ts | 15 ++++++---- packages/opencode/src/util/effect-zod.ts | 13 +++++++-- .../opencode/test/util/effect-zod.test.ts | 28 +++++++++++++++++-- 5 files changed, 48 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/config/tui-schema.ts b/packages/opencode/src/cli/cmd/tui/config/tui-schema.ts index 66569efea5..ed79e8e524 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui-schema.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui-schema.ts @@ -31,7 +31,7 @@ export const TuiInfo = z $schema: z.string().optional(), theme: z.string().optional(), keybinds: KeybindOverride.optional(), - plugin: ConfigPlugin.Spec.array().optional(), + plugin: ConfigPlugin.Spec.zod.array().optional(), plugin_enabled: z.record(z.string(), z.boolean()).optional(), }) .extend(TuiOptions.shape) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index f228878d04..0f6d71f447 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -113,7 +113,7 @@ export const Info = z "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.", ), // User-facing plugin config is stored as Specs; provenance gets attached later while configs are merged. - plugin: ConfigPlugin.Spec.array().optional(), + plugin: ConfigPlugin.Spec.zod.array().optional(), share: z .enum(["manual", "auto", "disabled"]) .optional() diff --git a/packages/opencode/src/config/plugin.ts b/packages/opencode/src/config/plugin.ts index 7d335bcc53..ebc3e2230d 100644 --- a/packages/opencode/src/config/plugin.ts +++ b/packages/opencode/src/config/plugin.ts @@ -1,16 +1,21 @@ import { Glob } from "@opencode-ai/shared/util/glob" -import z from "zod" +import { Schema } from "effect" import { pathToFileURL } from "url" import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" import path from "path" -const Options = z.record(z.string(), z.unknown()) -export type Options = z.infer +export const Options = Schema.Record(Schema.String, Schema.Unknown).pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Options = Schema.Schema.Type // Spec is the user-config value: either just a plugin identifier, or the identifier plus inline options. // It answers "what should we load?" but says nothing about where that value came from. -export const Spec = z.union([z.string(), z.tuple([z.string(), Options])]) -export type Spec = z.infer +export const Spec = Schema.Union([ + Schema.String, + Schema.mutable(Schema.Tuple([Schema.String, Options])), +]).pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Spec = Schema.Schema.Type export type Scope = "global" | "local" diff --git a/packages/opencode/src/util/effect-zod.ts b/packages/opencode/src/util/effect-zod.ts index c3240deaac..771795ba68 100644 --- a/packages/opencode/src/util/effect-zod.ts +++ b/packages/opencode/src/util/effect-zod.ts @@ -119,9 +119,16 @@ function object(ast: SchemaAST.Objects): z.ZodTypeAny { } function array(ast: SchemaAST.Arrays): z.ZodTypeAny { - if (ast.elements.length > 0) return fail(ast) - if (ast.rest.length !== 1) return fail(ast) - return z.array(walk(ast.rest[0])) + // Pure variadic arrays: { elements: [], rest: [item] } + if (ast.elements.length === 0) { + if (ast.rest.length !== 1) return fail(ast) + return z.array(walk(ast.rest[0])) + } + // Fixed-length tuples: { elements: [a, b, ...], rest: [] } + // Tuples with a variadic tail (...rest) are not yet supported. + if (ast.rest.length > 0) return fail(ast) + const items = ast.elements.map(walk) + return z.tuple(items as [z.ZodTypeAny, ...Array]) } function decl(ast: SchemaAST.Declaration): z.ZodTypeAny { diff --git a/packages/opencode/test/util/effect-zod.test.ts b/packages/opencode/test/util/effect-zod.test.ts index 1c84c96f3a..ba67a60e6d 100644 --- a/packages/opencode/test/util/effect-zod.test.ts +++ b/packages/opencode/test/util/effect-zod.test.ts @@ -61,8 +61,32 @@ describe("util.effect-zod", () => { }) }) - test("throws for unsupported tuple schemas", () => { - expect(() => zod(Schema.Tuple([Schema.String, Schema.Number]))).toThrow("unsupported effect schema") + describe("Tuples", () => { + test("fixed-length tuple parses matching array", () => { + const out = zod(Schema.Tuple([Schema.String, Schema.Number])) + expect(out.parse(["a", 1])).toEqual(["a", 1]) + expect(out.safeParse(["a"]).success).toBe(false) + expect(out.safeParse(["a", "b"]).success).toBe(false) + }) + + test("single-element tuple parses a one-element array", () => { + const out = zod(Schema.Tuple([Schema.Boolean])) + expect(out.parse([true])).toEqual([true]) + expect(out.safeParse([true, false]).success).toBe(false) + }) + + test("tuple inside a union picks the right branch", () => { + const out = zod(Schema.Union([Schema.String, Schema.Tuple([Schema.String, Schema.Number])])) + expect(out.parse("hello")).toBe("hello") + expect(out.parse(["foo", 42])).toEqual(["foo", 42]) + expect(out.safeParse(["foo"]).success).toBe(false) + }) + + test("plain arrays still work (no element positions)", () => { + const out = zod(Schema.Array(Schema.String)) + expect(out.parse(["a", "b", "c"])).toEqual(["a", "b", "c"]) + expect(out.parse([])).toEqual([]) + }) }) test("string literal unions produce z.enum with enum in JSON Schema", () => { From 89e8994fd15c4ac1235930a07e0b6e37254df22b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 21:08:00 +0000 Subject: [PATCH 141/335] chore: generate --- packages/opencode/src/config/plugin.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/config/plugin.ts b/packages/opencode/src/config/plugin.ts index ebc3e2230d..4277c1cd6d 100644 --- a/packages/opencode/src/config/plugin.ts +++ b/packages/opencode/src/config/plugin.ts @@ -11,10 +11,9 @@ export type Options = Schema.Schema.Type // Spec is the user-config value: either just a plugin identifier, or the identifier plus inline options. // It answers "what should we load?" but says nothing about where that value came from. -export const Spec = Schema.Union([ - Schema.String, - Schema.mutable(Schema.Tuple([Schema.String, Options])), -]).pipe(withStatics((s) => ({ zod: zod(s) }))) +export const Spec = Schema.Union([Schema.String, Schema.mutable(Schema.Tuple([Schema.String, Options]))]).pipe( + withStatics((s) => ({ zod: zod(s) })), +) export type Spec = Schema.Schema.Type export type Scope = "global" | "local" From 0068ccec35493e2657678a4ab0654a278bd14685 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 17 Apr 2026 16:27:32 -0500 Subject: [PATCH 142/335] fix: ensure copilot model list filters out disabled models (#23176) --- packages/opencode/src/plugin/github-copilot/copilot.ts | 1 - packages/opencode/src/plugin/github-copilot/models.ts | 9 ++++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/plugin/github-copilot/copilot.ts b/packages/opencode/src/plugin/github-copilot/copilot.ts index c9b7e3c1c7..c018f72bd5 100644 --- a/packages/opencode/src/plugin/github-copilot/copilot.ts +++ b/packages/opencode/src/plugin/github-copilot/copilot.ts @@ -1,6 +1,5 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin" import type { Model } from "@opencode-ai/sdk/v2" -import { Installation } from "@/installation" import { InstallationVersion } from "@/installation/version" import { iife } from "@/util/iife" import { Log } from "../../util" diff --git a/packages/opencode/src/plugin/github-copilot/models.ts b/packages/opencode/src/plugin/github-copilot/models.ts index 71d21afbe4..0aac0d3f5e 100644 --- a/packages/opencode/src/plugin/github-copilot/models.ts +++ b/packages/opencode/src/plugin/github-copilot/models.ts @@ -10,6 +10,11 @@ export const schema = z.object({ // every version looks like: `{model.id}-YYYY-MM-DD` version: z.string(), supported_endpoints: z.array(z.string()).optional(), + policy: z + .object({ + state: z.string().optional(), + }) + .optional(), capabilities: z.object({ family: z.string(), limits: z.object({ @@ -122,7 +127,9 @@ export async function get( }) const result = { ...existing } - const remote = new Map(data.data.filter((m) => m.model_picker_enabled).map((m) => [m.id, m] as const)) + const remote = new Map( + data.data.filter((m) => m.model_picker_enabled && m.policy?.state !== "disabled").map((m) => [m.id, m] as const), + ) // prune existing models whose api.id isn't in the endpoint response for (const [key, model] of Object.entries(result)) { From cded68a2e2e233b8026c50408771b25d0f4e9682 Mon Sep 17 00:00:00 2001 From: Dax Date: Fri, 17 Apr 2026 17:30:50 -0400 Subject: [PATCH 143/335] refactor(npm): use object-based package spec for install API (#23181) --- packages/opencode/src/cli/cmd/tui/config/tui.ts | 7 ++++++- packages/opencode/src/config/config.ts | 7 ++++++- packages/opencode/src/npm/index.ts | 16 +++++++++++----- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index 179046e026..9d5cd65bfd 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -158,7 +158,12 @@ export const layer = Layer.effect( (dir) => npm .install(dir, { - add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)], + add: [ + { + name: "@opencode-ai/plugin", + version: InstallationLocal ? undefined : InstallationVersion, + }, + ], }) .pipe(Effect.forkScoped), { diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 0f6d71f447..a2d62eaa5e 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -518,7 +518,12 @@ export const layer = Layer.effect( const dep = yield* npmSvc .install(dir, { - add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)], + add: [ + { + name: "@opencode-ai/plugin", + version: InstallationLocal ? undefined : InstallationVersion, + }, + ], }) .pipe( Effect.exit, diff --git a/packages/opencode/src/npm/index.ts b/packages/opencode/src/npm/index.ts index f242598192..d92099bc3c 100644 --- a/packages/opencode/src/npm/index.ts +++ b/packages/opencode/src/npm/index.ts @@ -25,7 +25,12 @@ export interface Interface { readonly add: (pkg: string) => Effect.Effect readonly install: ( dir: string, - input?: { add: string[] }, + input?: { + add: { + name: string + version?: string + }[] + }, ) => Effect.Effect readonly outdated: (pkg: string, cachedVersion: string) => Effect.Effect readonly which: (pkg: string) => Effect.Effect> @@ -137,17 +142,18 @@ export const layer = Layer.effect( return resolveEntryPoint(first.name, first.path) }, Effect.scoped) - const install = Effect.fn("Npm.install")(function* (dir: string, input?: { add: string[] }) { + const install: Interface["install"] = Effect.fn("Npm.install")(function* (dir, input) { const canWrite = yield* afs.access(dir, { writable: true }).pipe( Effect.as(true), Effect.orElseSucceed(() => false), ) if (!canWrite) return + const add = input?.add.map((pkg) => [pkg.name, pkg.version].filter(Boolean).join("@")) ?? [] yield* Effect.gen(function* () { const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules")) if (!nodeModulesExists) { - yield* reify({ add: input?.add, dir }) + yield* reify({ add, dir }) return } }).pipe(Effect.withSpan("Npm.checkNodeModules")) @@ -163,7 +169,7 @@ export const layer = Layer.effect( ...Object.keys(pkgAny?.devDependencies || {}), ...Object.keys(pkgAny?.peerDependencies || {}), ...Object.keys(pkgAny?.optionalDependencies || {}), - ...(input?.add || []), + ...(input?.add || []).map((pkg) => pkg.name), ]) const root = lockAny?.packages?.[""] || {} @@ -176,7 +182,7 @@ export const layer = Layer.effect( for (const name of declared) { if (!locked.has(name)) { - yield* reify({ dir, add: input?.add }) + yield* reify({ dir, add }) return } } From a35b8a95c27d28e979a3826e1289d7ee87f40251 Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 18 Apr 2026 00:29:16 +0000 Subject: [PATCH 144/335] release: v1.4.11 --- bun.lock | 32 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop-electron/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++----- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/shared/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 19 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 6e8845c4f2..3cb3cbea60 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.4.10", + "version": "1.4.11", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -83,7 +83,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.4.10", + "version": "1.4.11", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -117,7 +117,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.4.10", + "version": "1.4.11", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -144,7 +144,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.4.10", + "version": "1.4.11", "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/openai": "3.0.48", @@ -168,7 +168,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.4.10", + "version": "1.4.11", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -192,7 +192,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.4.10", + "version": "1.4.11", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -225,7 +225,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "1.4.10", + "version": "1.4.11", "dependencies": { "effect": "catalog:", "electron-context-menu": "4.1.2", @@ -268,7 +268,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.4.10", + "version": "1.4.11", "dependencies": { "@opencode-ai/shared": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -297,7 +297,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.4.10", + "version": "1.4.11", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -313,7 +313,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.4.10", + "version": "1.4.11", "bin": { "opencode": "./bin/opencode", }, @@ -458,7 +458,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.4.10", + "version": "1.4.11", "dependencies": { "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", @@ -493,7 +493,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.4.10", + "version": "1.4.11", "dependencies": { "cross-spawn": "catalog:", }, @@ -508,7 +508,7 @@ }, "packages/shared": { "name": "@opencode-ai/shared", - "version": "1.4.10", + "version": "1.4.11", "bin": { "opencode": "./bin/opencode", }, @@ -532,7 +532,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.4.10", + "version": "1.4.11", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -567,7 +567,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.4.10", + "version": "1.4.11", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -616,7 +616,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.4.10", + "version": "1.4.11", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 7206bd5394..5a1a4504ea 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.4.10", + "version": "1.4.11", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 2dbd630163..200a5e30e3 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.4.10", + "version": "1.4.11", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 323ee013df..f233726e69 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.4.10", + "version": "1.4.11", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index bef6f53e08..1142230bb7 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.4.10", + "version": "1.4.11", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 5b56f5678d..860150aa28 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.4.10", + "version": "1.4.11", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index dbf63e8500..8142b12ada 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "1.4.10", + "version": "1.4.11", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 97c8713a2b..a23342bdec 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.4.10", + "version": "1.4.11", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 914978d7da..f565159628 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.4.10", + "version": "1.4.11", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 55b3673739..32039c097a 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.4.10" +version = "1.4.11" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 1dc1f32bb1..5d4229f64f 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.4.10", + "version": "1.4.11", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 7d24d7ca31..2acbc4fe84 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.4.10", + "version": "1.4.11", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index cc1f24b68e..15cd2db6e2 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.4.10", + "version": "1.4.11", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 3a6d13680e..91d6647449 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.4.10", + "version": "1.4.11", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/shared/package.json b/packages/shared/package.json index 615cd42c00..a8cd62886b 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.4.10", + "version": "1.4.11", "name": "@opencode-ai/shared", "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index eb23c2036f..8ca990ba58 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.4.10", + "version": "1.4.11", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index e756c33fca..98cb928b7b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.4.10", + "version": "1.4.11", "type": "module", "license": "MIT", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index 0c4a0c6480..194f44ec03 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.4.10", + "version": "1.4.11", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 83d00c987a..f52135c206 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.4.10", + "version": "1.4.11", "publisher": "sst-dev", "repository": { "type": "git", From d5c4c26b4b1a62271c7278ffc81485577a46a8e4 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 18:43:10 -0400 Subject: [PATCH 145/335] =?UTF-8?q?feat(server):=20auto-tag=20route=20span?= =?UTF-8?q?s=20with=20route=20params=20(session.id,=20message.id,=20?= =?UTF-8?q?=E2=80=A6)=20(#23189)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/server/routes/instance/trace.ts | 35 +++++++++---- .../test/server/trace-attributes.test.ts | 52 +++++++++++++++++++ 2 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 packages/opencode/test/server/trace-attributes.test.ts diff --git a/packages/opencode/src/server/routes/instance/trace.ts b/packages/opencode/src/server/routes/instance/trace.ts index 3e1f72d8b2..fca313b745 100644 --- a/packages/opencode/src/server/routes/instance/trace.ts +++ b/packages/opencode/src/server/routes/instance/trace.ts @@ -4,18 +4,31 @@ import { AppRuntime } from "@/effect/app-runtime" type AppEnv = Parameters[0] extends Effect.Effect ? R : never +// Build the base span attributes for an HTTP handler: method, path, and every +// matched route param (sessionID, messageID, partID, providerID, ptyID, …) +// prefixed with `opencode.`. This makes each request's root span searchable +// by ID in motel without having to parse the path string. +export interface RequestLike { + readonly req: { + readonly method: string + readonly url: string + param(): Record + } +} + +export function requestAttributes(c: RequestLike): Record { + const attributes: Record = { + "http.method": c.req.method, + "http.path": new URL(c.req.url).pathname, + } + for (const [key, value] of Object.entries(c.req.param())) { + attributes[`opencode.${key}`] = value + } + return attributes +} + export function runRequest(name: string, c: Context, effect: Effect.Effect) { - const url = new URL(c.req.url) - return AppRuntime.runPromise( - effect.pipe( - Effect.withSpan(name, { - attributes: { - "http.method": c.req.method, - "http.path": url.pathname, - }, - }), - ), - ) + return AppRuntime.runPromise(effect.pipe(Effect.withSpan(name, { attributes: requestAttributes(c) }))) } export async function jsonRequest( diff --git a/packages/opencode/test/server/trace-attributes.test.ts b/packages/opencode/test/server/trace-attributes.test.ts new file mode 100644 index 0000000000..376c81fc62 --- /dev/null +++ b/packages/opencode/test/server/trace-attributes.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test } from "bun:test" +import { requestAttributes } from "../../src/server/routes/instance/trace" + +function fakeContext(method: string, url: string, params: Record) { + return { + req: { + method, + url, + param: () => params, + }, + } +} + +describe("requestAttributes", () => { + test("includes http method and path", () => { + const attrs = requestAttributes(fakeContext("GET", "http://localhost/session", {})) + expect(attrs["http.method"]).toBe("GET") + expect(attrs["http.path"]).toBe("/session") + }) + + test("strips query string from path", () => { + const attrs = requestAttributes(fakeContext("GET", "http://localhost/file/search?query=foo&limit=10", {})) + expect(attrs["http.path"]).toBe("/file/search") + }) + + test("tags route params with opencode. prefix", () => { + const attrs = requestAttributes( + fakeContext("GET", "http://localhost/session/ses_abc/message/msg_def/part/prt_ghi", { + sessionID: "ses_abc", + messageID: "msg_def", + partID: "prt_ghi", + }), + ) + expect(attrs["opencode.sessionID"]).toBe("ses_abc") + expect(attrs["opencode.messageID"]).toBe("msg_def") + expect(attrs["opencode.partID"]).toBe("prt_ghi") + }) + + test("produces no param attributes when no params are matched", () => { + const attrs = requestAttributes(fakeContext("POST", "http://localhost/config", {})) + expect(Object.keys(attrs).filter((k) => k.startsWith("opencode."))).toEqual([]) + }) + + test("handles non-ID params (e.g. mcp :name) without mangling", () => { + const attrs = requestAttributes( + fakeContext("POST", "http://localhost/mcp/exa/connect", { + name: "exa", + }), + ) + expect(attrs["opencode.name"]).toBe("exa") + }) +}) From b5aba5807cfbcafc57ffd488cbcb0148f8f1f4d6 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 18:47:48 -0400 Subject: [PATCH 146/335] feat(tui): show session ID in sidebar on non-prod channels (#23185) --- packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index 4a7b711a03..6d92752efe 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -3,7 +3,7 @@ import { useSync } from "@tui/context/sync" import { createMemo, Show } from "solid-js" import { useTheme } from "../../context/theme" import { useTuiConfig } from "../../context/tui-config" -import { InstallationVersion } from "@/installation/version" +import { InstallationChannel, InstallationVersion } from "@/installation/version" import { TuiPluginRuntime } from "../../plugin" import { getScrollAcceleration } from "../../util/scroll" @@ -62,6 +62,9 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { {session()!.title} + + {props.sessionID} + {" "} From 7b98f544ff6b58f8dde9421bd1e7bf3e8c395d4f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 19:10:34 -0400 Subject: [PATCH 147/335] feat(effect-zod): add catchall (StructWithRest) support to the walker (#23186) --- packages/opencode/src/util/effect-zod.ts | 16 ++++- .../opencode/test/util/effect-zod.test.ts | 69 +++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/util/effect-zod.ts b/packages/opencode/src/util/effect-zod.ts index 771795ba68..22c6eda42d 100644 --- a/packages/opencode/src/util/effect-zod.ts +++ b/packages/opencode/src/util/effect-zod.ts @@ -107,15 +107,27 @@ function union(ast: SchemaAST.Union): z.ZodTypeAny { } function object(ast: SchemaAST.Objects): z.ZodTypeAny { + // Pure record: { [k: string]: V } if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 1) { const sig = ast.indexSignatures[0] if (sig.parameter._tag !== "String") return fail(ast) return z.record(z.string(), walk(sig.type)) } - if (ast.indexSignatures.length > 0) return fail(ast) + // Pure object with known fields and no index signatures. + if (ast.indexSignatures.length === 0) { + return z.object(Object.fromEntries(ast.propertySignatures.map((sig) => [String(sig.name), walk(sig.type)]))) + } - return z.object(Object.fromEntries(ast.propertySignatures.map((sig) => [String(sig.name), walk(sig.type)]))) + // Struct with a catchall (StructWithRest): known fields + index signature. + // Only supports a single string-keyed index signature; multi-signature or + // symbol/number keys fall through to fail. + if (ast.indexSignatures.length !== 1) return fail(ast) + const sig = ast.indexSignatures[0] + if (sig.parameter._tag !== "String") return fail(ast) + return z + .object(Object.fromEntries(ast.propertySignatures.map((p) => [String(p.name), walk(p.type)]))) + .catchall(walk(sig.type)) } function array(ast: SchemaAST.Arrays): z.ZodTypeAny { diff --git a/packages/opencode/test/util/effect-zod.test.ts b/packages/opencode/test/util/effect-zod.test.ts index ba67a60e6d..89234e7265 100644 --- a/packages/opencode/test/util/effect-zod.test.ts +++ b/packages/opencode/test/util/effect-zod.test.ts @@ -263,4 +263,73 @@ describe("util.effect-zod", () => { expect(result.error!.issues[0].message).toBe("missing 'required' key") }) }) + + describe("StructWithRest / catchall", () => { + test("struct with a string-keyed record rest parses known AND extra keys", () => { + const schema = zod( + Schema.StructWithRest( + Schema.Struct({ + apiKey: Schema.optional(Schema.String), + baseURL: Schema.optional(Schema.String), + }), + [Schema.Record(Schema.String, Schema.Unknown)], + ), + ) + + // Known fields come through as declared + expect(schema.parse({ apiKey: "sk-x" })).toEqual({ apiKey: "sk-x" }) + + // Extra keys are preserved (catchall) + expect( + schema.parse({ + apiKey: "sk-x", + baseURL: "https://api.example.com", + customField: "anything", + nested: { foo: 1 }, + }), + ).toEqual({ + apiKey: "sk-x", + baseURL: "https://api.example.com", + customField: "anything", + nested: { foo: 1 }, + }) + }) + + test("catchall value type constrains the extras", () => { + const schema = zod( + Schema.StructWithRest( + Schema.Struct({ + count: Schema.Number, + }), + [Schema.Record(Schema.String, Schema.Number)], + ), + ) + + // Known field + numeric extras + expect(schema.parse({ count: 10, a: 1, b: 2 })).toEqual({ count: 10, a: 1, b: 2 }) + + // Non-numeric extra is rejected + expect(schema.safeParse({ count: 10, bad: "not a number" }).success).toBe(false) + }) + + test("JSON schema output marks additionalProperties appropriately", () => { + const schema = zod( + Schema.StructWithRest( + Schema.Struct({ + id: Schema.String, + }), + [Schema.Record(Schema.String, Schema.Unknown)], + ), + ) + const shape = json(schema) as { additionalProperties?: unknown } + // Presence of `additionalProperties` (truthy or a schema) signals catchall. + expect(shape.additionalProperties).not.toBe(false) + expect(shape.additionalProperties).toBeDefined() + }) + + test("plain struct without rest still emits additionalProperties unchanged (regression)", () => { + const schema = zod(Schema.Struct({ id: Schema.String })) + expect(schema.parse({ id: "x" })).toEqual({ id: "x" }) + }) + }) }) From eafbe5c57c41528b52974b671793896e1aa98e96 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 19:29:33 -0400 Subject: [PATCH 148/335] refactor(server): align route-span attrs with OTel semantic conventions (#23198) --- .../src/server/routes/instance/trace.ts | 21 ++++++++-- .../test/server/trace-attributes.test.ts | 38 +++++++++++++++---- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/packages/opencode/src/server/routes/instance/trace.ts b/packages/opencode/src/server/routes/instance/trace.ts index fca313b745..4c7119ef3a 100644 --- a/packages/opencode/src/server/routes/instance/trace.ts +++ b/packages/opencode/src/server/routes/instance/trace.ts @@ -5,9 +5,12 @@ import { AppRuntime } from "@/effect/app-runtime" type AppEnv = Parameters[0] extends Effect.Effect ? R : never // Build the base span attributes for an HTTP handler: method, path, and every -// matched route param (sessionID, messageID, partID, providerID, ptyID, …) -// prefixed with `opencode.`. This makes each request's root span searchable -// by ID in motel without having to parse the path string. +// matched route param. Names follow OTel attribute-naming guidance: +// domain-first (`session.id`, `message.id`, …) so they match the existing +// OTel `session.id` semantic convention and the bare `message.id` we +// already emit from Tool.execute. Non-standard route params fall back to +// `opencode.` since those are internal implementation details +// (per https://opentelemetry.io/blog/2025/how-to-name-your-span-attributes/). export interface RequestLike { readonly req: { readonly method: string @@ -16,13 +19,23 @@ export interface RequestLike { } } +// Normalize a Hono route param key (e.g. `sessionID`, `messageID`, `name`) +// to an OTel attribute key. `fooID` → `foo.id` for ID-shaped params; any +// other param is namespaced under `opencode.` to avoid colliding with +// standard conventions. +export function paramToAttributeKey(key: string): string { + const m = key.match(/^(.+)ID$/) + if (m) return `${m[1].toLowerCase()}.id` + return `opencode.${key}` +} + export function requestAttributes(c: RequestLike): Record { const attributes: Record = { "http.method": c.req.method, "http.path": new URL(c.req.url).pathname, } for (const [key, value] of Object.entries(c.req.param())) { - attributes[`opencode.${key}`] = value + attributes[paramToAttributeKey(key)] = value } return attributes } diff --git a/packages/opencode/test/server/trace-attributes.test.ts b/packages/opencode/test/server/trace-attributes.test.ts index 376c81fc62..c6e8005a20 100644 --- a/packages/opencode/test/server/trace-attributes.test.ts +++ b/packages/opencode/test/server/trace-attributes.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { requestAttributes } from "../../src/server/routes/instance/trace" +import { paramToAttributeKey, requestAttributes } from "../../src/server/routes/instance/trace" function fakeContext(method: string, url: string, params: Record) { return { @@ -11,6 +11,25 @@ function fakeContext(method: string, url: string, params: Record } } +describe("paramToAttributeKey", () => { + test("converts fooID to foo.id", () => { + expect(paramToAttributeKey("sessionID")).toBe("session.id") + expect(paramToAttributeKey("messageID")).toBe("message.id") + expect(paramToAttributeKey("partID")).toBe("part.id") + expect(paramToAttributeKey("projectID")).toBe("project.id") + expect(paramToAttributeKey("providerID")).toBe("provider.id") + expect(paramToAttributeKey("ptyID")).toBe("pty.id") + expect(paramToAttributeKey("permissionID")).toBe("permission.id") + expect(paramToAttributeKey("requestID")).toBe("request.id") + expect(paramToAttributeKey("workspaceID")).toBe("workspace.id") + }) + + test("namespaces non-ID params under opencode.", () => { + expect(paramToAttributeKey("name")).toBe("opencode.name") + expect(paramToAttributeKey("slug")).toBe("opencode.slug") + }) +}) + describe("requestAttributes", () => { test("includes http method and path", () => { const attrs = requestAttributes(fakeContext("GET", "http://localhost/session", {})) @@ -23,7 +42,7 @@ describe("requestAttributes", () => { expect(attrs["http.path"]).toBe("/file/search") }) - test("tags route params with opencode. prefix", () => { + test("emits OTel-style .id for ID-shaped route params", () => { const attrs = requestAttributes( fakeContext("GET", "http://localhost/session/ses_abc/message/msg_def/part/prt_ghi", { sessionID: "ses_abc", @@ -31,22 +50,27 @@ describe("requestAttributes", () => { partID: "prt_ghi", }), ) - expect(attrs["opencode.sessionID"]).toBe("ses_abc") - expect(attrs["opencode.messageID"]).toBe("msg_def") - expect(attrs["opencode.partID"]).toBe("prt_ghi") + expect(attrs["session.id"]).toBe("ses_abc") + expect(attrs["message.id"]).toBe("msg_def") + expect(attrs["part.id"]).toBe("prt_ghi") + // No camelCase leftovers: + expect(attrs["opencode.sessionID"]).toBeUndefined() + expect(attrs["opencode.messageID"]).toBeUndefined() + expect(attrs["opencode.partID"]).toBeUndefined() }) test("produces no param attributes when no params are matched", () => { const attrs = requestAttributes(fakeContext("POST", "http://localhost/config", {})) - expect(Object.keys(attrs).filter((k) => k.startsWith("opencode."))).toEqual([]) + expect(Object.keys(attrs).filter((k) => k !== "http.method" && k !== "http.path")).toEqual([]) }) - test("handles non-ID params (e.g. mcp :name) without mangling", () => { + test("namespaces non-ID params under opencode. (e.g. mcp :name)", () => { const attrs = requestAttributes( fakeContext("POST", "http://localhost/mcp/exa/connect", { name: "exa", }), ) expect(attrs["opencode.name"]).toBe("exa") + expect(attrs["name"]).toBeUndefined() }) }) From 2899984819f49b2e1119021d313f78caa0db0e2f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 19:29:53 -0400 Subject: [PATCH 149/335] refactor(config): migrate provider (Model + Info) to Effect Schema (#23197) --- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/config/provider.ts | 220 +++++++++++------------ 2 files changed, 109 insertions(+), 113 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index a2d62eaa5e..bfb0c2f1f4 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -171,7 +171,7 @@ export const Info = z .optional() .describe("Agent configuration, see https://opencode.ai/docs/agents"), provider: z - .record(z.string(), ConfigProvider.Info) + .record(z.string(), ConfigProvider.Info.zod) .optional() .describe("Custom provider configurations and model overrides"), mcp: z diff --git a/packages/opencode/src/config/provider.ts b/packages/opencode/src/config/provider.ts index 877677519f..4664999de8 100644 --- a/packages/opencode/src/config/provider.ts +++ b/packages/opencode/src/config/provider.ts @@ -1,120 +1,116 @@ +import { Schema } from "effect" import z from "zod" +import { zod, ZodOverride } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" -export const Model = z - .object({ - id: z.string(), - name: z.string(), - family: z.string().optional(), - release_date: z.string(), - attachment: z.boolean(), - reasoning: z.boolean(), - temperature: z.boolean(), - tool_call: z.boolean(), - interleaved: z - .union([ - z.literal(true), - z - .object({ - field: z.enum(["reasoning_content", "reasoning_details"]), - }) - .strict(), - ]) - .optional(), - cost: z - .object({ - input: z.number(), - output: z.number(), - cache_read: z.number().optional(), - cache_write: z.number().optional(), - context_over_200k: z - .object({ - input: z.number(), - output: z.number(), - cache_read: z.number().optional(), - cache_write: z.number().optional(), - }) - .optional(), - }) - .optional(), - limit: z.object({ - context: z.number(), - input: z.number().optional(), - output: z.number(), +// Positive integer preserving exact Zod JSON Schema (type: integer, exclusiveMinimum: 0). +const PositiveInt = Schema.Number.annotate({ + [ZodOverride]: z.number().int().positive(), +}) + +export const Model = Schema.Struct({ + id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + family: Schema.optional(Schema.String), + release_date: Schema.optional(Schema.String), + attachment: Schema.optional(Schema.Boolean), + reasoning: Schema.optional(Schema.Boolean), + temperature: Schema.optional(Schema.Boolean), + tool_call: Schema.optional(Schema.Boolean), + interleaved: Schema.optional( + Schema.Union([ + Schema.Literal(true), + Schema.Struct({ + field: Schema.Literals(["reasoning_content", "reasoning_details"]), + }), + ]), + ), + cost: Schema.optional( + Schema.Struct({ + input: Schema.Number, + output: Schema.Number, + cache_read: Schema.optional(Schema.Number), + cache_write: Schema.optional(Schema.Number), + context_over_200k: Schema.optional( + Schema.Struct({ + input: Schema.Number, + output: Schema.Number, + cache_read: Schema.optional(Schema.Number), + cache_write: Schema.optional(Schema.Number), + }), + ), }), - modalities: z - .object({ - input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), - output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), - }) - .optional(), - experimental: z.boolean().optional(), - status: z.enum(["alpha", "beta", "deprecated"]).optional(), - provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(), - options: z.record(z.string(), z.any()), - headers: z.record(z.string(), z.string()).optional(), - variants: z - .record( - z.string(), - z - .object({ - disabled: z.boolean().optional().describe("Disable this variant for the model"), - }) - .catchall(z.any()), - ) - .optional() - .describe("Variant-specific configuration"), - }) - .partial() + ), + limit: Schema.optional( + Schema.Struct({ + context: Schema.Number, + input: Schema.optional(Schema.Number), + output: Schema.Number, + }), + ), + modalities: Schema.optional( + Schema.Struct({ + input: Schema.mutable(Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"]))), + output: Schema.mutable(Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"]))), + }), + ), + experimental: Schema.optional(Schema.Boolean), + status: Schema.optional(Schema.Literals(["alpha", "beta", "deprecated"])), + provider: Schema.optional(Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) })), + options: Schema.optional(Schema.Record(Schema.String, Schema.Any)), + headers: Schema.optional(Schema.Record(Schema.String, Schema.String)), + variants: Schema.optional( + Schema.Record( + Schema.String, + Schema.StructWithRest( + Schema.Struct({ + disabled: Schema.optional(Schema.Boolean).annotate({ description: "Disable this variant for the model" }), + }), + [Schema.Record(Schema.String, Schema.Any)], + ), + ).annotate({ description: "Variant-specific configuration" }), + ), +}).pipe(withStatics((s) => ({ zod: zod(s) }))) -export const Info = z - .object({ - api: z.string().optional(), - name: z.string(), - env: z.array(z.string()), - id: z.string(), - npm: z.string().optional(), - whitelist: z.array(z.string()).optional(), - blacklist: z.array(z.string()).optional(), - options: z - .object({ - apiKey: z.string().optional(), - baseURL: z.string().optional(), - enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"), - setCacheKey: z.boolean().optional().describe("Enable promptCacheKey for this provider (default false)"), - timeout: z - .union([ - z - .number() - .int() - .positive() - .describe( - "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", - ), - z.literal(false).describe("Disable timeout for this provider entirely."), - ]) - .optional() - .describe( +export class Info extends Schema.Class("ProviderConfig")({ + api: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + env: Schema.optional(Schema.mutable(Schema.Array(Schema.String))), + id: Schema.optional(Schema.String), + npm: Schema.optional(Schema.String), + whitelist: Schema.optional(Schema.mutable(Schema.Array(Schema.String))), + blacklist: Schema.optional(Schema.mutable(Schema.Array(Schema.String))), + options: Schema.optional( + Schema.StructWithRest( + Schema.Struct({ + apiKey: Schema.optional(Schema.String), + baseURL: Schema.optional(Schema.String), + enterpriseUrl: Schema.optional(Schema.String).annotate({ + description: "GitHub Enterprise URL for copilot authentication", + }), + setCacheKey: Schema.optional(Schema.Boolean).annotate({ + description: "Enable promptCacheKey for this provider (default false)", + }), + timeout: Schema.optional( + Schema.Union([PositiveInt, Schema.Literal(false)]).annotate({ + description: + "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", + }), + ).annotate({ + description: "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", - ), - chunkTimeout: z - .number() - .int() - .positive() - .optional() - .describe( + }), + chunkTimeout: Schema.optional(PositiveInt).annotate({ + description: "Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.", - ), - }) - .catchall(z.any()) - .optional(), - models: z.record(z.string(), Model).optional(), - }) - .partial() - .strict() - .meta({ - ref: "ProviderConfig", - }) - -export type Info = z.infer + }), + }), + [Schema.Record(Schema.String, Schema.Any)], + ), + ), + models: Schema.optional(Schema.Record(Schema.String, Model)), +}) { + static readonly zod = zod(this) +} export * as ConfigProvider from "./provider" From cf0a53c501d069832be51640d1264cf658f381c9 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 23:30:51 +0000 Subject: [PATCH 150/335] chore: generate --- packages/opencode/src/config/provider.ts | 4 +++- packages/sdk/openapi.json | 8 ++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/config/provider.ts b/packages/opencode/src/config/provider.ts index 4664999de8..b435f43759 100644 --- a/packages/opencode/src/config/provider.ts +++ b/packages/opencode/src/config/provider.ts @@ -56,7 +56,9 @@ export const Model = Schema.Struct({ ), experimental: Schema.optional(Schema.Boolean), status: Schema.optional(Schema.Literals(["alpha", "beta", "deprecated"])), - provider: Schema.optional(Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) })), + provider: Schema.optional( + Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }), + ), options: Schema.optional(Schema.Record(Schema.String, Schema.Any)), headers: Schema.optional(Schema.Record(Schema.String, Schema.String)), variants: Schema.optional( diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index c3fd00356c..5a93c4db2a 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -11180,13 +11180,11 @@ "description": "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", "anyOf": [ { - "description": "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, { - "description": "Disable timeout for this provider entirely.", "type": "boolean", "const": false } @@ -11247,8 +11245,7 @@ "enum": ["reasoning_content", "reasoning_details"] } }, - "required": ["field"], - "additionalProperties": false + "required": ["field"] } ] }, @@ -11377,8 +11374,7 @@ } } } - }, - "additionalProperties": false + } }, "McpLocalConfig": { "type": "object", From 211136e3a8cfdfcca658d97d5b0a1ce7b1b0a940 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 19:55:55 -0400 Subject: [PATCH 151/335] feat(effect-zod): transform support + walk memoization + flattened checks (#23203) --- packages/opencode/src/util/effect-zod.ts | 80 ++++++++-- .../opencode/test/util/effect-zod.test.ts | 148 +++++++++++++++++- 2 files changed, 213 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/util/effect-zod.ts b/packages/opencode/src/util/effect-zod.ts index 22c6eda42d..6f46c684be 100644 --- a/packages/opencode/src/util/effect-zod.ts +++ b/packages/opencode/src/util/effect-zod.ts @@ -1,4 +1,4 @@ -import { Schema, SchemaAST } from "effect" +import { Effect, Option, Schema, SchemaAST } from "effect" import z from "zod" /** @@ -8,33 +8,85 @@ import z from "zod" */ export const ZodOverride: unique symbol = Symbol.for("effect-zod/override") +// AST nodes are immutable and frequently shared across schemas (e.g. a single +// Schema.Class embedded in multiple parents). Memoizing by node identity +// avoids rebuilding equivalent Zod subtrees and keeps derived children stable +// by reference across callers. +const walkCache = new WeakMap() + +// Shared empty ParseOptions for the rare callers that need one — avoids +// allocating a fresh object per parse inside refinements and transforms. +const EMPTY_PARSE_OPTIONS = {} as SchemaAST.ParseOptions + export function zod(schema: S): z.ZodType> { return walk(schema.ast) as z.ZodType> } function walk(ast: SchemaAST.AST): z.ZodTypeAny { + const cached = walkCache.get(ast) + if (cached) return cached + const result = walkUncached(ast) + walkCache.set(ast, result) + return result +} + +function walkUncached(ast: SchemaAST.AST): z.ZodTypeAny { const override = (ast.annotations as any)?.[ZodOverride] as z.ZodTypeAny | undefined if (override) return override - let out = body(ast) - for (const check of ast.checks ?? []) { - out = applyCheck(out, check, ast) - } + // Schema.Class wraps its fields in a Declaration AST plus an encoding that + // constructs the class instance. For the Zod derivation we want the plain + // field shape (the decoded/consumer view), not the class instance — so + // Declarations fall through to body(), not encoded(). User-level + // Schema.decodeTo / Schema.transform attach encoding to non-Declaration + // nodes, where we do apply the transform. + const hasTransform = ast.encoding?.length && ast._tag !== "Declaration" + const base = hasTransform ? encoded(ast) : body(ast) + const out = ast.checks?.length ? applyChecks(base, ast.checks, ast) : base const desc = SchemaAST.resolveDescription(ast) const ref = SchemaAST.resolveIdentifier(ast) - const next = desc ? out.describe(desc) : out - return ref ? next.meta({ ref }) : next + const described = desc ? out.describe(desc) : out + return ref ? described.meta({ ref }) : described } -function applyCheck(out: z.ZodTypeAny, check: SchemaAST.Check, ast: SchemaAST.AST): z.ZodTypeAny { - if (check._tag === "FilterGroup") { - return check.checks.reduce((acc, sub) => applyCheck(acc, sub, ast), out) +// Walk the encoded side and apply each link's decode to produce the decoded +// shape. A node `Target` produced by `from.decodeTo(Target)` carries +// `Target.encoding = [Link(from, transformation)]`. Chained decodeTo calls +// nest the encoding via `Link.to` so walking it recursively threads all +// prior transforms — typical encoding.length is 1. +function encoded(ast: SchemaAST.AST): z.ZodTypeAny { + const encoding = ast.encoding! + return encoding.reduce((acc, link) => acc.transform((v) => decode(link.transformation, v)), walk(encoding[0].to)) +} + +// Transformations built via pure `SchemaGetter.transform(fn)` (the common +// decodeTo case) resolve synchronously, so running with no services is safe. +// Effectful / middleware-based transforms will surface as Effect defects. +function decode(transformation: SchemaAST.Link["transformation"], value: unknown): unknown { + const exit = Effect.runSyncExit( + (transformation.decode as any).run(Option.some(value), EMPTY_PARSE_OPTIONS) as Effect.Effect>, + ) + if (exit._tag === "Failure") throw new Error(`effect-zod: transform failed: ${String(exit.cause)}`) + return Option.getOrElse(exit.value, () => value) +} + +// Flatten FilterGroups and any nested variants into a linear list of Filters +// so we can run all of them inside a single Zod .superRefine wrapper instead +// of stacking N wrapper layers (one per check). +function applyChecks(out: z.ZodTypeAny, checks: SchemaAST.Checks, ast: SchemaAST.AST): z.ZodTypeAny { + const filters: SchemaAST.Filter[] = [] + const collect = (c: SchemaAST.Check) => { + if (c._tag === "FilterGroup") c.checks.forEach(collect) + else filters.push(c) } + checks.forEach(collect) return out.superRefine((value, ctx) => { - const issue = check.run(value, ast, {} as any) - if (!issue) return - const message = issueMessage(issue) ?? (check.annotations as any)?.message ?? "Validation failed" - ctx.addIssue({ code: "custom", message }) + for (const filter of filters) { + const issue = filter.run(value, ast, EMPTY_PARSE_OPTIONS) + if (!issue) continue + const message = issueMessage(issue) ?? (filter.annotations as any)?.message ?? "Validation failed" + ctx.addIssue({ code: "custom", message }) + } }) } diff --git a/packages/opencode/test/util/effect-zod.test.ts b/packages/opencode/test/util/effect-zod.test.ts index 89234e7265..3d72984bfc 100644 --- a/packages/opencode/test/util/effect-zod.test.ts +++ b/packages/opencode/test/util/effect-zod.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { Schema } from "effect" +import { Schema, SchemaGetter } from "effect" import z from "zod" import { zod, ZodOverride } from "../../src/util/effect-zod" @@ -332,4 +332,150 @@ describe("util.effect-zod", () => { expect(schema.parse({ id: "x" })).toEqual({ id: "x" }) }) }) + + describe("transforms (Schema.decodeTo)", () => { + test("Number -> pseudo-Duration (seconds) applies the decode function", () => { + // Models the account/account.ts DurationFromSeconds pattern. + const SecondsToMs = Schema.Number.pipe( + Schema.decodeTo(Schema.Number, { + decode: SchemaGetter.transform((n: number) => n * 1000), + encode: SchemaGetter.transform((ms: number) => ms / 1000), + }), + ) + + const schema = zod(SecondsToMs) + expect(schema.parse(3)).toBe(3000) + expect(schema.parse(0)).toBe(0) + }) + + test("String -> Number via parseInt decode", () => { + const ParsedInt = Schema.String.pipe( + Schema.decodeTo(Schema.Number, { + decode: SchemaGetter.transform((s: string) => Number.parseInt(s, 10)), + encode: SchemaGetter.transform((n: number) => String(n)), + }), + ) + + const schema = zod(ParsedInt) + expect(schema.parse("42")).toBe(42) + expect(schema.parse("0")).toBe(0) + }) + + test("transform inside a struct field applies per-field", () => { + const Field = Schema.Number.pipe( + Schema.decodeTo(Schema.Number, { + decode: SchemaGetter.transform((n: number) => n + 1), + encode: SchemaGetter.transform((n: number) => n - 1), + }), + ) + + const schema = zod( + Schema.Struct({ + plain: Schema.Number, + bumped: Field, + }), + ) + + expect(schema.parse({ plain: 5, bumped: 10 })).toEqual({ plain: 5, bumped: 11 }) + }) + + test("chained decodeTo composes transforms in order", () => { + // String -> Number (parseInt) -> Number (doubled). + // Exercises the encoded() reduce, not just a single link. + const Chained = Schema.String.pipe( + Schema.decodeTo(Schema.Number, { + decode: SchemaGetter.transform((s: string) => Number.parseInt(s, 10)), + encode: SchemaGetter.transform((n: number) => String(n)), + }), + Schema.decodeTo(Schema.Number, { + decode: SchemaGetter.transform((n: number) => n * 2), + encode: SchemaGetter.transform((n: number) => n / 2), + }), + ) + + const schema = zod(Chained) + expect(schema.parse("21")).toBe(42) + expect(schema.parse("0")).toBe(0) + }) + + test("Schema.Class is unaffected by transform walker (returns plain object, not instance)", () => { + // Schema.Class uses Declaration + encoding under the hood to construct + // class instances. The walker must NOT apply that transform, or zod + // parsing would return class instances instead of plain objects. + class Method extends Schema.Class("TxTestMethod")({ + type: Schema.String, + value: Schema.Number, + }) {} + + const schema = zod(Method) + const parsed = schema.parse({ type: "oauth", value: 1 }) + expect(parsed).toEqual({ type: "oauth", value: 1 }) + // Guardrail: ensure we didn't get back a Method instance. + expect(parsed).not.toBeInstanceOf(Method) + }) + }) + + describe("optimizations", () => { + test("walk() memoizes by AST identity — same AST node returns same Zod", () => { + const shared = Schema.Struct({ id: Schema.String, name: Schema.String }) + const left = zod(shared) + const right = zod(shared) + expect(left).toBe(right) + }) + + test("nested reuse of the same AST reuses the cached Zod child", () => { + // Two different parents embed the same inner schema. The inner zod + // child should be identical by reference inside both parents. + class Inner extends Schema.Class("MemoTestInner")({ + value: Schema.String, + }) {} + + class OuterA extends Schema.Class("MemoTestOuterA")({ + inner: Inner, + }) {} + + class OuterB extends Schema.Class("MemoTestOuterB")({ + inner: Inner, + }) {} + + const shapeA = (zod(OuterA) as any).shape ?? (zod(OuterA) as any)._def?.shape?.() + const shapeB = (zod(OuterB) as any).shape ?? (zod(OuterB) as any)._def?.shape?.() + expect(shapeA.inner).toBe(shapeB.inner) + }) + + test("multiple checks run in a single refinement layer (all fire on one value)", () => { + // Three checks attached to the same schema. All three must run and + // report — asserting that no check silently got dropped when we + // flattened into one superRefine. + const positive = Schema.makeFilter((n: number) => (n > 0 ? undefined : "not positive")) + const even = Schema.makeFilter((n: number) => (n % 2 === 0 ? undefined : "not even")) + const under100 = Schema.makeFilter((n: number) => (n < 100 ? undefined : "too big")) + + const schema = zod(Schema.Number.check(positive).check(even).check(under100)) + + const neg = schema.safeParse(-3) + expect(neg.success).toBe(false) + expect(neg.error!.issues.map((i) => i.message)).toEqual(expect.arrayContaining(["not positive", "not even"])) + + const big = schema.safeParse(101) + expect(big.success).toBe(false) + expect(big.error!.issues.map((i) => i.message)).toContain("too big") + + // Passing value satisfies all three + expect(schema.parse(42)).toBe(42) + }) + + test("FilterGroup flattens into the single refinement layer alongside its siblings", () => { + const positive = Schema.makeFilter((n: number) => (n > 0 ? undefined : "not positive")) + const even = Schema.makeFilter((n: number) => (n % 2 === 0 ? undefined : "not even")) + const group = Schema.makeFilterGroup([positive, even]) + const under100 = Schema.makeFilter((n: number) => (n < 100 ? undefined : "too big")) + + const schema = zod(Schema.Number.check(group).check(under100)) + + const bad = schema.safeParse(-3) + expect(bad.success).toBe(false) + expect(bad.error!.issues.map((i) => i.message)).toEqual(expect.arrayContaining(["not positive", "not even"])) + }) + }) }) From c4816f944e6cbc1ff4c03484189c11f5fcd45e9c Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 17 Apr 2026 23:56:50 +0000 Subject: [PATCH 152/335] chore: generate --- packages/opencode/src/util/effect-zod.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/util/effect-zod.ts b/packages/opencode/src/util/effect-zod.ts index 6f46c684be..82c661e402 100644 --- a/packages/opencode/src/util/effect-zod.ts +++ b/packages/opencode/src/util/effect-zod.ts @@ -56,7 +56,10 @@ function walkUncached(ast: SchemaAST.AST): z.ZodTypeAny { // prior transforms — typical encoding.length is 1. function encoded(ast: SchemaAST.AST): z.ZodTypeAny { const encoding = ast.encoding! - return encoding.reduce((acc, link) => acc.transform((v) => decode(link.transformation, v)), walk(encoding[0].to)) + return encoding.reduce( + (acc, link) => acc.transform((v) => decode(link.transformation, v)), + walk(encoding[0].to), + ) } // Transformations built via pure `SchemaGetter.transform(fn)` (the common @@ -64,7 +67,9 @@ function encoded(ast: SchemaAST.AST): z.ZodTypeAny { // Effectful / middleware-based transforms will surface as Effect defects. function decode(transformation: SchemaAST.Link["transformation"], value: unknown): unknown { const exit = Effect.runSyncExit( - (transformation.decode as any).run(Option.some(value), EMPTY_PARSE_OPTIONS) as Effect.Effect>, + (transformation.decode as any).run(Option.some(value), EMPTY_PARSE_OPTIONS) as Effect.Effect< + Option.Option + >, ) if (exit._tag === "Failure") throw new Error(`effect-zod: transform failed: ${String(exit.cause)}`) return Option.getOrElse(exit.value, () => value) From b493dabfe6edac3b671d9c369892a99d24650916 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 20:06:30 -0400 Subject: [PATCH 153/335] docs(effect): refresh migration status specs (#23206) --- packages/opencode/specs/effect/facades.md | 81 ++++++++----------- packages/opencode/specs/effect/http-api.md | 54 ++++++------- .../opencode/specs/effect/instance-context.md | 20 ++--- packages/opencode/specs/effect/loose-ends.md | 14 ++-- packages/opencode/specs/effect/migration.md | 78 ++++++++---------- packages/opencode/specs/effect/routes.md | 34 ++++---- .../opencode/specs/effect/server-package.md | 36 +++++---- packages/opencode/specs/effect/tools.md | 8 +- 8 files changed, 147 insertions(+), 178 deletions(-) diff --git a/packages/opencode/specs/effect/facades.md b/packages/opencode/specs/effect/facades.md index e2d9d3d8a1..8bf7d97bad 100644 --- a/packages/opencode/specs/effect/facades.md +++ b/packages/opencode/specs/effect/facades.md @@ -1,12 +1,13 @@ # Facade removal checklist -Concrete inventory of the remaining `makeRuntime(...)`-backed service facades in `packages/opencode`. +Concrete inventory of the remaining `makeRuntime(...)`-backed facades in `packages/opencode`. -As of 2026-04-13, latest `origin/dev`: +Current status on this branch: -- `src/` still has 15 `makeRuntime(...)` call sites. -- 13 of those are still in scope for facade removal. -- 2 are excluded from this checklist: `bus/index.ts` and `effect/cross-spawn-spawner.ts`. +- `src/` has 5 `makeRuntime(...)` call sites total. +- 2 are intentionally excluded from this checklist: `src/bus/index.ts` and `src/effect/cross-spawn-spawner.ts`. +- 1 is tracked primarily by the instance-context migration rather than facade removal: `src/project/instance.ts`. +- That leaves 2 live runtime-backed service facades still worth tracking here: `src/npm/index.ts` and `src/cli/cmd/tui/config/tui.ts`. Recent progress: @@ -15,8 +16,9 @@ Recent progress: ## Priority hotspots -- `server/instance/session.ts` still depends on `Session`, `SessionPrompt`, `SessionRevert`, `SessionCompaction`, `SessionSummary`, `ShareSession`, `Agent`, and `Permission` facades. -- `src/effect/app-runtime.ts` still references many facade namespaces directly, so it should stay in view during each deletion. +- `src/cli/cmd/tui/config/tui.ts` still exports `makeRuntime(...)` plus async facade helpers for `get()` and `waitForDependencies()`. +- `src/npm/index.ts` still exports `makeRuntime(...)` plus async facade helpers for `install()`, `add()`, `outdated()`, and `which()`. +- `src/project/instance.ts` still uses a dedicated runtime for project boot, but that file is really part of the broader legacy instance-context transition tracked in `instance-context.md`. ## Completed Batches @@ -184,53 +186,34 @@ These were the recurring mistakes and useful corrections from the first two batc 5. For CLI readability, extract file-local preload helpers when the handler starts doing config load + service load + batched effect fanout inline. 6. When rebasing a facade branch after nearby merges, prefer the already-cleaned service/test version over older inline facade-era code. -## Next batch +## Remaining work -Recommended next five, in order: +Most of the original facade-removal backlog is already done. The practical remaining work is narrower now: -1. `src/permission/index.ts` -2. `src/agent/agent.ts` -3. `src/session/summary.ts` -4. `src/session/revert.ts` -5. `src/mcp/auth.ts` - -Why this batch: - -- It keeps pushing the session-adjacent cleanup without jumping straight into `session/index.ts` or `session/prompt.ts`. -- `Permission`, `Agent`, `SessionSummary`, and `SessionRevert` all reduce fanout in `server/instance/session.ts`. -- `McpAuth` is small and closely related to the just-landed `MCP` cleanup. - -After that batch, the expected follow-up is the main session cluster: - -1. `src/session/index.ts` -2. `src/session/prompt.ts` -3. `src/session/compaction.ts` +1. remove the `Npm` runtime-backed facade from `src/npm/index.ts` +2. remove the `TuiConfig` runtime-backed facade from `src/cli/cmd/tui/config/tui.ts` +3. keep `src/project/instance.ts` in the separate instance-context migration, not this checklist ## Checklist -- [ ] `src/session/index.ts` (`Session`) - facades: `create`, `fork`, `get`, `setTitle`, `setArchived`, `setPermission`, `setRevert`, `messages`, `children`, `remove`, `updateMessage`, `removeMessage`, `removePart`, `updatePart`; main callers: `server/instance/session.ts`, `cli/cmd/session.ts`, `cli/cmd/export.ts`, `cli/cmd/github.ts`; tests: `test/server/session-actions.test.ts`, `test/server/session-list.test.ts`, `test/server/global-session-list.test.ts` -- [ ] `src/session/prompt.ts` (`SessionPrompt`) - facades: `prompt`, `resolvePromptParts`, `cancel`, `loop`, `shell`, `command`; main callers: `server/instance/session.ts`, `cli/cmd/github.ts`; tests: `test/session/prompt.test.ts`, `test/session/prompt-effect.test.ts`, `test/session/structured-output-integration.test.ts` -- [ ] `src/session/revert.ts` (`SessionRevert`) - facades: `revert`, `unrevert`, `cleanup`; main callers: `server/instance/session.ts`; tests: `test/session/revert-compact.test.ts` -- [ ] `src/session/compaction.ts` (`SessionCompaction`) - facades: `isOverflow`, `prune`, `create`; main callers: `server/instance/session.ts`; tests: `test/session/compaction.test.ts` -- [ ] `src/session/summary.ts` (`SessionSummary`) - facades: `summarize`, `diff`; main callers: `session/prompt.ts`, `session/processor.ts`, `server/instance/session.ts`; tests: `test/session/snapshot-tool-race.test.ts` -- [ ] `src/share/session.ts` (`ShareSession`) - facades: `create`, `share`, `unshare`; main callers: `server/instance/session.ts`, `cli/cmd/github.ts` -- [ ] `src/agent/agent.ts` (`Agent`) - facades: `get`, `list`, `defaultAgent`, `generate`; main callers: `cli/cmd/agent.ts`, `server/instance/session.ts`, `server/instance/experimental.ts`; tests: `test/agent/agent.test.ts` -- [ ] `src/permission/index.ts` (`Permission`) - facades: `ask`, `reply`, `list`; main callers: `server/instance/permission.ts`, `server/instance/session.ts`, `session/llm.ts`; tests: `test/permission/next.test.ts` -- [x] `src/file/index.ts` (`File`) - facades removed and merged. -- [x] `src/lsp/index.ts` (`LSP`) - facades removed and merged. -- [x] `src/mcp/index.ts` (`MCP`) - facades removed and merged. -- [x] `src/config/config.ts` (`Config`) - facades removed and merged. -- [x] `src/provider/provider.ts` (`Provider`) - facades removed and merged. -- [x] `src/pty/index.ts` (`Pty`) - facades removed and merged. -- [x] `src/skill/index.ts` (`Skill`) - facades removed and merged. -- [x] `src/project/vcs.ts` (`Vcs`) - facades removed and merged. -- [x] `src/tool/registry.ts` (`ToolRegistry`) - facades removed and merged. -- [ ] `src/worktree/index.ts` (`Worktree`) - facades: `makeWorktreeInfo`, `createFromInfo`, `create`, `remove`, `reset`; main callers: `control-plane/adaptors/worktree.ts`, `server/instance/experimental.ts`; tests: `test/project/worktree.test.ts`, `test/project/worktree-remove.test.ts` -- [x] `src/auth/index.ts` (`Auth`) - facades removed and merged. -- [ ] `src/mcp/auth.ts` (`McpAuth`) - facades: `get`, `getForUrl`, `all`, `set`, `remove`, `updateTokens`, `updateClientInfo`, `updateCodeVerifier`, `updateOAuthState`; main callers: `mcp/oauth-provider.ts`, `cli/cmd/mcp.ts`; tests: `test/mcp/oauth-auto-connect.test.ts` -- [ ] `src/plugin/index.ts` (`Plugin`) - facades: `trigger`, `list`, `init`; main callers: `agent/agent.ts`, `session/llm.ts`, `project/bootstrap.ts`; tests: `test/plugin/trigger.test.ts`, `test/provider/provider.test.ts` -- [ ] `src/project/project.ts` (`Project`) - facades: `fromDirectory`, `discover`, `initGit`, `update`, `sandboxes`, `addSandbox`, `removeSandbox`; main callers: `project/instance.ts`, `server/instance/project.ts`, `server/instance/experimental.ts`; tests: `test/project/project.test.ts`, `test/project/migrate-global.test.ts` -- [ ] `src/snapshot/index.ts` (`Snapshot`) - facades: `init`, `track`, `patch`, `restore`, `revert`, `diff`, `diffFull`; main callers: `project/bootstrap.ts`, `cli/cmd/debug/snapshot.ts`; tests: `test/snapshot/snapshot.test.ts`, `test/session/revert-compact.test.ts` +- [ ] `src/npm/index.ts` (`Npm`) - still exports runtime-backed async facade helpers on top of `Npm.Service` +- [ ] `src/cli/cmd/tui/config/tui.ts` (`TuiConfig`) - still exports runtime-backed async facade helpers on top of `TuiConfig.Service` +- [x] `src/session/session.ts` / `src/session/prompt.ts` / `src/session/revert.ts` / `src/session/summary.ts` - service-local facades removed +- [x] `src/agent/agent.ts` (`Agent`) - service-local facades removed +- [x] `src/permission/index.ts` (`Permission`) - service-local facades removed +- [x] `src/worktree/index.ts` (`Worktree`) - service-local facades removed +- [x] `src/plugin/index.ts` (`Plugin`) - service-local facades removed +- [x] `src/snapshot/index.ts` (`Snapshot`) - service-local facades removed +- [x] `src/file/index.ts` (`File`) - facades removed and merged +- [x] `src/lsp/index.ts` (`LSP`) - facades removed and merged +- [x] `src/mcp/index.ts` (`MCP`) - facades removed and merged +- [x] `src/config/config.ts` (`Config`) - facades removed and merged +- [x] `src/provider/provider.ts` (`Provider`) - facades removed and merged +- [x] `src/pty/index.ts` (`Pty`) - facades removed and merged +- [x] `src/skill/index.ts` (`Skill`) - facades removed and merged +- [x] `src/project/vcs.ts` (`Vcs`) - facades removed and merged +- [x] `src/tool/registry.ts` (`ToolRegistry`) - facades removed and merged +- [x] `src/auth/index.ts` (`Auth`) - facades removed and merged ## Excluded `makeRuntime(...)` sites diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index 09cb86a000..93ef81a325 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -76,7 +76,7 @@ Many route boundaries still use Zod-first validators. That does not block all ex ### Mixed handler styles -Many current `server/instance/*.ts` handlers still call async facades directly. Migrating those to composed `Effect.gen(...)` handlers is the low-risk step to do first. +Many current `server/routes/instance/*.ts` handlers still mix composed Effect code with smaller Promise- or ALS-backed seams. Migrating those to consistent `Effect.gen(...)` handlers is the low-risk step to do first. ### Non-JSON routes @@ -90,7 +90,7 @@ The current server composition, middleware, and docs flow are Hono-centered toda ### 1. Finish the prerequisites first -- continue route-handler effectification in `server/instance/*.ts` +- continue route-handler effectification in `server/routes/instance/*.ts` - continue schema migration toward Effect Schema-first DTOs and errors - keep removing service facades @@ -98,9 +98,9 @@ The current server composition, middleware, and docs flow are Hono-centered toda Introduce one small `HttpApi` group for plain JSON endpoints only. Good initial candidates are the least stateful endpoints in: -- `server/instance/question.ts` -- `server/instance/provider.ts` -- `server/instance/permission.ts` +- `server/routes/instance/question.ts` +- `server/routes/instance/provider.ts` +- `server/routes/instance/permission.ts` Avoid `session.ts`, SSE, websocket, and TUI-facing routes first. @@ -155,9 +155,9 @@ This gives: As each route group is ported to `HttpApi`: -1. change its `root` path from `/experimental/httpapi/` to `/` -2. add `.all("/", handler)` / `.all("//*", handler)` to the flag block in `instance/index.ts` -3. for partial ports (e.g. only `GET /provider/auth`), bridge only the specific path +1. add `.get(...)` / `.post(...)` bridge entries to the flag block in `server/routes/instance/index.ts` +2. for partial ports (e.g. only `GET /provider/auth`), bridge only the specific path +3. keep the legacy Hono route registered behind it for OpenAPI / SDK generation until the spec pipeline changes 4. verify SDK output is unchanged Leave streaming-style endpoints on Hono until there is a clear reason to move them. @@ -267,7 +267,7 @@ Use the same sequence for each route group. 3. Apply the schema migration ordering above so those types are Effect Schema-first. 4. Define the `HttpApi` contract separately from the handlers. 5. Implement handlers by yielding the existing service from context. -6. Mount the new surface in parallel under an experimental prefix. +6. Mount the new surface in parallel behind the `OPENCODE_EXPERIMENTAL_HTTPAPI` bridge. 7. Regenerate the SDK and verify zero diff against `dev` (see SDK shape rule above). 8. Add one end-to-end test and one OpenAPI-focused test. 9. Compare ergonomics before migrating the next endpoint. @@ -286,20 +286,20 @@ Placement rule: - keep `HttpApi` code under `src/server`, not `src/effect` - `src/effect` should stay focused on runtimes, layers, instance state, and shared Effect plumbing - place each `HttpApi` slice next to the HTTP boundary it serves -- for instance-scoped routes, prefer `src/server/instance/httpapi/*` -- if control-plane routes ever migrate, prefer `src/server/control/httpapi/*` +- for instance-scoped routes, prefer `src/server/routes/instance/httpapi/*` +- if control-plane routes ever migrate, prefer `src/server/routes/control/httpapi/*` Suggested file layout for a repeatable spike: -- `src/server/instance/httpapi/question.ts` — contract and handler layer for one route group -- `src/server/instance/httpapi/server.ts` — standalone Effect HTTP server that composes all groups -- `test/server/question-httpapi.test.ts` — end-to-end test against the real service +- `src/server/routes/instance/httpapi/question.ts` — contract and handler layer for one route group +- `src/server/routes/instance/httpapi/server.ts` — bridged Effect HTTP layer that composes all groups +- route or OpenAPI verification should live alongside the existing server tests; there is no dedicated `question-httpapi` test file on this branch Suggested responsibilities: - `question.ts` defines the `HttpApi` contract and `HttpApiBuilder.group(...)` handlers -- `server.ts` composes all route groups into one `HttpRouter.serve` layer with shared middleware (auth, instance lookup) -- tests use `ExperimentalHttpApiServer.layerTest` to run against a real in-process HTTP server +- `server.ts` composes all route groups into one `HttpRouter.toWebHandler(...)` bridge with shared middleware (auth, instance lookup) +- tests should verify the bridged routes through the normal server surface ## Example migration shape @@ -319,33 +319,33 @@ Each route-group spike should follow the same shape. - keep handler bodies thin - keep transport mapping at the HTTP boundary only -### 3. Standalone server +### 3. Bridged server -- the Effect HTTP server is self-contained in `httpapi/server.ts` -- it is **not** mounted into the Hono app — no bridge, no `toWebHandler` -- route paths use the `/experimental/httpapi` prefix so they match the eventual cutover -- each route group exposes its own OpenAPI doc endpoint +- the Effect HTTP layer is composed in `httpapi/server.ts` +- it is mounted into the Hono app via `HttpRouter.toWebHandler(...)` +- routes keep their normal instance paths and are gated by the `OPENCODE_EXPERIMENTAL_HTTPAPI` flag +- the legacy Hono handlers stay registered after the bridge so current OpenAPI / SDK generation still works ### 4. Verification - seed real state through the existing service -- call the experimental endpoints +- call the bridged endpoints with the flag enabled - assert that the service behavior is unchanged - assert that the generated OpenAPI contains the migrated paths and schemas ## Boundary composition -The standalone Effect server owns its own middleware stack. It does not share middleware with the Hono server. +The Effect `HttpApi` layer owns its own auth and instance middleware, but it is currently mounted inside the existing Hono server. ### Auth -- the standalone server implements auth as an `HttpApiMiddleware.Service` using `HttpApiSecurity.basic` +- the bridged `HttpApi` layer implements auth as an `HttpApiMiddleware.Service` using `HttpApiSecurity.basic` - each route group's `HttpApi` is wrapped with `.middleware(Authorization)` before being served -- this is independent of the Hono `AuthMiddleware` — when the Effect server eventually replaces Hono, this becomes the only auth layer +- this is independent of the Hono auth layer; the current bridge keeps the responsibility local to the `HttpApi` slice ### Instance and workspace lookup -- the standalone server resolves instance context via an `HttpRouter.middleware` that reads `x-opencode-directory` headers and `directory` query params +- the bridged `HttpApi` layer resolves instance context via an `HttpRouter.middleware` that reads `x-opencode-directory` headers and `directory` query params - this is the Effect equivalent of the Hono `WorkspaceRouterMiddleware` - `HttpApi` handlers yield services from context and assume the correct instance has already been provided @@ -360,7 +360,7 @@ The standalone Effect server owns its own middleware stack. It does not share mi The first slice is successful if: -- the standalone Effect server starts and serves the endpoints independently of the Hono server +- the bridged endpoints serve correctly through the existing Hono host when the flag is enabled - the handlers reuse the existing Effect service - request decoding and response shapes are schema-defined from canonical Effect schemas - any remaining Zod boundary usage is derived from `.zod` or clearly temporary diff --git a/packages/opencode/specs/effect/instance-context.md b/packages/opencode/specs/effect/instance-context.md index 6c160a9477..7d0d7eb13c 100644 --- a/packages/opencode/specs/effect/instance-context.md +++ b/packages/opencode/specs/effect/instance-context.md @@ -157,7 +157,7 @@ Direct legacy usage means any source file that still calls one of: - `Instance.reload(...)` - `Instance.dispose()` / `Instance.disposeAll()` -Current total: `54` files in `packages/opencode/src`. +Current total: `56` files in `packages/opencode/src`. ### Core bridge and plumbing @@ -177,13 +177,13 @@ Migration rule: These are the current request-entry seams that still create or consume instance context through the legacy helper. -- `src/server/instance/middleware.ts` -- `src/server/instance/index.ts` -- `src/server/instance/project.ts` -- `src/server/instance/workspace.ts` -- `src/server/instance/file.ts` -- `src/server/instance/experimental.ts` -- `src/server/instance/global.ts` +- `src/server/routes/instance/middleware.ts` +- `src/server/routes/instance/index.ts` +- `src/server/routes/instance/project.ts` +- `src/server/routes/control/workspace.ts` +- `src/server/routes/instance/file.ts` +- `src/server/routes/instance/experimental.ts` +- `src/server/routes/global.ts` Migration rule: @@ -239,7 +239,7 @@ Migration rule: These modules are already the best near-term migration targets because they are in Effect code but still read sync getters from the legacy helper. - `src/agent/agent.ts` -- `src/config/tui-migrate.ts` +- `src/cli/cmd/tui/config/tui-migrate.ts` - `src/file/index.ts` - `src/file/watcher.ts` - `src/format/formatter.ts` @@ -250,7 +250,7 @@ These modules are already the best near-term migration targets because they are - `src/project/vcs.ts` - `src/provider/provider.ts` - `src/pty/index.ts` -- `src/session/index.ts` +- `src/session/session.ts` - `src/session/instruction.ts` - `src/session/llm.ts` - `src/session/system.ts` diff --git a/packages/opencode/specs/effect/loose-ends.md b/packages/opencode/specs/effect/loose-ends.md index a2fed492b3..4e7ada7ff9 100644 --- a/packages/opencode/specs/effect/loose-ends.md +++ b/packages/opencode/specs/effect/loose-ends.md @@ -4,11 +4,11 @@ Small follow-ups that do not fit neatly into the main facade, route, tool, or sc ## Config / TUI -- [ ] `config/tui.ts` - finish the internal Effect migration after the `Instance.state(...)` removal. +- [ ] `cli/cmd/tui/config/tui.ts` - finish the internal Effect migration. Keep the current precedence and migration semantics intact while converting the remaining internal async helpers (`loadState`, `mergeFile`, `loadFile`, `load`) to `Effect.gen(...)` / `Effect.fn(...)`. -- [ ] `config/tui.ts` callers - once the internal service is stable, migrate plain async callers to use `TuiConfig.Service` directly where that actually simplifies the code. +- [ ] `cli/cmd/tui/config/tui.ts` callers - once the internal service is stable, migrate plain async callers to use `TuiConfig.Service` directly where that actually simplifies the code. Likely first callers: `cli/cmd/tui/attach.ts`, `cli/cmd/tui/thread.ts`, `cli/cmd/tui/plugin/runtime.ts`. -- [ ] `env/index.ts` - move the last production `Instance.state(...)` usage onto `InstanceState` (or its replacement) so `Instance.state` can be deleted. +- [x] `env/index.ts` - already uses `InstanceState.make(...)`. ## ConfigPaths @@ -21,14 +21,12 @@ Small follow-ups that do not fit neatly into the main facade, route, tool, or sc - `readFile(...)` - `parseText(...)` - [ ] `config/config.ts` - switch internal config loading from `Effect.promise(() => ConfigPaths.*(...))` to `yield* paths.*(...)` once the service exists. -- [ ] `config/tui.ts` - switch TUI config loading from async `ConfigPaths.*` wrappers to the `ConfigPaths.Service` once that service exists. -- [ ] `config/tui-migrate.ts` - decide whether to leave this as a plain async module using wrapper functions or effectify it fully after `ConfigPaths.Service` lands. +- [ ] `cli/cmd/tui/config/tui.ts` - switch TUI config loading from async `ConfigPaths.*` wrappers to the `ConfigPaths.Service` once that service exists. +- [ ] `cli/cmd/tui/config/tui-migrate.ts` - decide whether to leave this as a plain async module using wrapper functions or effectify it fully after `ConfigPaths.Service` lands. ## Instance cleanup -- [ ] `project/instance.ts` - remove `Instance.state(...)` once `env/index.ts` is migrated. -- [ ] `project/state.ts` - delete the bespoke per-instance state helper after the last production caller is gone. -- [ ] `test/project/state.test.ts` - replace or delete the old `Instance.state(...)` tests after the removal. +- [ ] `project/instance.ts` - keep shrinking the legacy ALS / Promise cache after the remaining `Instance.*` callers move over. ## Notes diff --git a/packages/opencode/specs/effect/migration.md b/packages/opencode/specs/effect/migration.md index b8bf4e0494..947eef5a15 100644 --- a/packages/opencode/specs/effect/migration.md +++ b/packages/opencode/specs/effect/migration.md @@ -19,53 +19,43 @@ See `instance-context.md` for the phased plan to remove the legacy ALS / promise ## Service shape -Every service follows the same pattern — a single namespace with the service definition, layer, `runPromise`, and async facade functions: +Every service follows the same pattern: one module, flat top-level exports, traced Effect methods, and a self-reexport at the bottom when the file is the public module. ```ts -export namespace Foo { - export interface Interface { - readonly get: (id: FooID) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/Foo") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - // For instance-scoped services: - const state = yield* InstanceState.make( - Effect.fn("Foo.state")(() => Effect.succeed({ ... })), - ) - - const get = Effect.fn("Foo.get")(function* (id: FooID) { - const s = yield* InstanceState.get(state) - // ... - }) - - return Service.of({ get }) - }), - ) - - // Optional: wire dependencies - export const defaultLayer = layer.pipe(Layer.provide(FooDep.layer)) - - // Per-service runtime (inside the namespace) - const { runPromise } = makeRuntime(Service, defaultLayer) - - // Async facade functions - export async function get(id: FooID) { - return runPromise((svc) => svc.get(id)) - } +export interface Interface { + readonly get: (id: FooID) => Effect.Effect } + +export class Service extends Context.Service()("@opencode/Foo") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const state = yield* InstanceState.make( + Effect.fn("Foo.state")(() => Effect.succeed({ ... })), + ) + + const get = Effect.fn("Foo.get")(function* (id: FooID) { + const s = yield* InstanceState.get(state) + // ... + }) + + return Service.of({ get }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(FooDep.layer)) + +export * as Foo from "." ``` Rules: -- Keep everything in one namespace, one file — no separate `service.ts` / `index.ts` split -- `runPromise` goes inside the namespace (not exported unless tests need it) -- Facade functions are plain `async function` — no `fn()` wrappers -- Use `Effect.fn("Namespace.method")` for all Effect functions (for tracing) -- No `Layer.fresh` — InstanceState handles per-directory isolation +- Keep the service surface in one module; prefer flat top-level exports over `export namespace Foo { ... }` +- Use `Effect.fn("Foo.method")` for Effect methods +- Use a self-reexport (`export * as Foo from "."` or `"./foo"`) for the public namespace projection +- Avoid service-local `makeRuntime(...)` facades unless a file is still intentionally in the older migration phase +- No `Layer.fresh` for normal per-directory isolation; use `InstanceState` ## Schema → Zod interop @@ -266,7 +256,7 @@ Tool-specific filesystem cleanup notes live in `tools.md`. ## Destroying the facades -This phase is still broadly open. As of 2026-04-13 there are still 15 `makeRuntime(...)` call sites under `src/`, with 13 still in scope for facade removal. The live checklist now lives in `facades.md`. +This phase is no longer broadly open. There are 5 `makeRuntime(...)` call sites under `src/`, and only a small subset are still ordinary facade-removal targets. The live checklist now lives in `facades.md`. These facades exist because cyclic imports used to force each service to build its own independent runtime. Now that the layer DAG is acyclic and `AppRuntime` (`src/effect/app-runtime.ts`) composes everything into one `ManagedRuntime`, we're removing them. @@ -297,11 +287,11 @@ For each service, the migration is roughly: - `ShareNext` — migrated 2026-04-11. Swapped remaining async callers to `AppRuntime.runPromise(ShareNext.Service.use(...))`, removed the `makeRuntime(...)` facade, and kept instance bootstrap on the shared app runtime. - `SessionTodo` — migrated 2026-04-10. Already matched the target service shape in `session/todo.ts`: single namespace, traced Effect methods, and no `makeRuntime(...)` facade remained; checklist updated to reflect the completed migration. - `Storage` — migrated 2026-04-10. One production caller (`Session.diff`) and all storage.test.ts tests converted to effectful style. Facades and `makeRuntime` removed. -- `SessionRunState` — migrated 2026-04-11. Single caller in `server/instance/session.ts` converted; facade removed. -- `Account` — migrated 2026-04-11. Callers in `server/instance/experimental.ts` and `cli/cmd/account.ts` converted; facade removed. +- `SessionRunState` — migrated 2026-04-11. Single caller in `server/routes/instance/session.ts` converted; facade removed. +- `Account` — migrated 2026-04-11. Callers in `server/routes/instance/experimental.ts` and `cli/cmd/account.ts` converted; facade removed. - `Instruction` — migrated 2026-04-11. Test-only callers converted; facade removed. - `FileWatcher` — migrated 2026-04-11. Callers in `project/bootstrap.ts` and test converted; facade removed. -- `Question` — migrated 2026-04-11. Callers in `server/instance/question.ts` and test converted; facade removed. +- `Question` — migrated 2026-04-11. Callers in `server/routes/instance/question.ts` and test converted; facade removed. - `Truncate` — migrated 2026-04-11. Caller in `tool/tool.ts` and test converted; facade removed. ## Route handler effectification diff --git a/packages/opencode/specs/effect/routes.md b/packages/opencode/specs/effect/routes.md index f6a61d2342..3bf7e1b556 100644 --- a/packages/opencode/specs/effect/routes.md +++ b/packages/opencode/specs/effect/routes.md @@ -39,28 +39,26 @@ This eliminates multiple `runPromise` round-trips and lets handlers compose natu ## Current route files -Current instance route files live under `src/server/instance`, not `server/routes`. +Current instance route files live under `src/server/routes/instance`. -The main migration targets are: +Files that are already mostly on the intended service-yielding shape: -- [ ] `server/instance/session.ts` — heaviest; still has many direct facade calls for Session, SessionPrompt, SessionRevert, SessionCompaction, SessionShare, SessionSummary, Agent, Bus -- [ ] `server/instance/global.ts` — still has direct facade calls for Config and instance lifecycle actions -- [ ] `server/instance/provider.ts` — still has direct facade calls for Config and Provider -- [ ] `server/instance/question.ts` — partially converted; still worth tracking here until it consistently uses the composed style -- [ ] `server/instance/pty.ts` — still calls Pty facades directly -- [ ] `server/instance/experimental.ts` — mixed state; some handlers are already composed, others still use facades +- [x] `server/routes/instance/question.ts` — handlers yield `Question.Service` +- [x] `server/routes/instance/provider.ts` — handlers yield `Provider.Service`, `ProviderAuth.Service`, and `Config.Service` +- [x] `server/routes/instance/permission.ts` — handlers yield `Permission.Service` +- [x] `server/routes/instance/mcp.ts` — handlers mostly yield `MCP.Service` +- [x] `server/routes/instance/pty.ts` — handlers yield `Pty.Service` -Additional route files that still participate in the migration: +Files still worth tracking here: -- [ ] `server/instance/index.ts` — Vcs, Agent, Skill, LSP, Format -- [ ] `server/instance/file.ts` — Ripgrep, File, LSP -- [ ] `server/instance/mcp.ts` — MCP facade-heavy -- [ ] `server/instance/permission.ts` — Permission -- [ ] `server/instance/workspace.ts` — Workspace -- [ ] `server/instance/tui.ts` — Bus and Session -- [ ] `server/instance/middleware.ts` — Session and Workspace lookups +- [ ] `server/routes/instance/session.ts` — still the heaviest mixed file; many handlers are composed, but the file still mixes patterns and has direct `Bus.publish(...)` / `Session.list(...)` usage +- [ ] `server/routes/instance/index.ts` — mostly converted, but still has direct `Instance.dispose()` / `Instance.*` reads for `/instance/dispose` and `/path` +- [ ] `server/routes/instance/file.ts` — most handlers yield services, but `/find` still passes `Instance.directory` directly into ripgrep and `/find/symbol` is still stubbed +- [ ] `server/routes/instance/experimental.ts` — mixed state; many handlers are composed, but some still rely on `runRequest(...)` or direct `Instance.project` reads +- [ ] `server/routes/instance/middleware.ts` — still enters the instance via `Instance.provide(...)` +- [ ] `server/routes/global.ts` — still uses `Instance.disposeAll()` and remains partly outside the fully-composed style ## Notes -- Some handlers already use `AppRuntime.runPromise(Effect.gen(...))` in isolated places. Keep pushing those files toward one consistent style. -- Route conversion is closely tied to facade removal. As services lose `makeRuntime`-backed async exports, route handlers should switch to yielding the service directly. +- Route conversion is now less about facade removal and more about removing the remaining direct `Instance.*` reads, `Instance.provide(...)` boundaries, and small Promise-style bridges inside route files. +- `jsonRequest(...)` / `runRequest(...)` already provide a good intermediate shape for many handlers. The remaining cleanup is mostly consistency work in the heavier files. diff --git a/packages/opencode/specs/effect/server-package.md b/packages/opencode/specs/effect/server-package.md index 10be7b9aed..06e89c18de 100644 --- a/packages/opencode/specs/effect/server-package.md +++ b/packages/opencode/specs/effect/server-package.md @@ -40,13 +40,13 @@ Everything still lives in `packages/opencode`. Important current facts: - there is no `packages/core` or `packages/cli` workspace yet -- `packages/server` now exists as a minimal scaffold package, but it does not own any real route contracts, handlers, or runtime composition yet +- there is no `packages/server` workspace yet on this branch - the main host server is still Hono-based in `src/server/server.ts` - current OpenAPI generation is Hono-based through `Server.openapi()` and `cli/cmd/generate.ts` - the Effect runtime and app layer are centralized in `src/effect/app-runtime.ts` and `src/effect/run-service.ts` -- there is already one experimental Effect `HttpApi` slice at `src/server/instance/httpapi/question.ts` -- that experimental slice is mounted under `/experimental/httpapi/question` -- that experimental slice already has an end-to-end test at `test/server/question-httpapi.test.ts` +- there are already bridged Effect `HttpApi` slices under `src/server/routes/instance/httpapi/*` +- those slices are mounted into the Hono server behind `OPENCODE_EXPERIMENTAL_HTTPAPI` +- the bridge currently covers `question`, `permission`, `provider`, partial `config`, and partial `project` routes This means the package split should start from an extraction path, not from greenfield package ownership. @@ -209,17 +209,19 @@ Current host and route composition: - `src/server/server.ts` - `src/server/control/index.ts` -- `src/server/instance/index.ts` +- `src/server/routes/instance/index.ts` - `src/server/middleware.ts` - `src/server/adapter.bun.ts` - `src/server/adapter.node.ts` -Current experimental `HttpApi` slice: +Current bridged `HttpApi` slices: -- `src/server/instance/httpapi/question.ts` -- `src/server/instance/httpapi/index.ts` -- `src/server/instance/experimental.ts` -- `test/server/question-httpapi.test.ts` +- `src/server/routes/instance/httpapi/question.ts` +- `src/server/routes/instance/httpapi/permission.ts` +- `src/server/routes/instance/httpapi/provider.ts` +- `src/server/routes/instance/httpapi/config.ts` +- `src/server/routes/instance/httpapi/project.ts` +- `src/server/routes/instance/httpapi/server.ts` Current OpenAPI flow: @@ -245,7 +247,7 @@ Keep in `packages/opencode` for now: - `src/server/server.ts` - `src/server/control/index.ts` -- `src/server/instance/*.ts` +- `src/server/routes/**/*.ts` - `src/server/middleware.ts` - `src/server/adapter.*.ts` - `src/effect/app-runtime.ts` @@ -305,14 +307,13 @@ Bad early migration targets: ## First vertical slice -The first slice for the package split is the existing experimental `question` group. +The first slice for the package split is still the existing `question` `HttpApi` group. Why `question` first: - it already exists as an experimental `HttpApi` slice - it already follows the desired contract and implementation split in one file - it is already mounted through the current Hono host -- it already has an end-to-end test - it is JSON-only - it has low blast radius @@ -357,7 +358,7 @@ Done means: Scope: -- extract the pure `HttpApi` contract from `src/server/instance/httpapi/question.ts` +- extract the pure `HttpApi` contract from `src/server/routes/instance/httpapi/question.ts` - place it in `packages/server/src/definition/question.ts` - aggregate it in `packages/server/src/definition/api.ts` - generate OpenAPI in `packages/server/src/openapi.ts` @@ -399,8 +400,9 @@ Scope: - replace local experimental question route wiring in `packages/opencode` - keep the same mount path: -- `/experimental/httpapi/question` -- `/experimental/httpapi/question/doc` +- `/question` +- `/question/:requestID/reply` +- `/question/:requestID/reject` Rules: @@ -569,7 +571,7 @@ For package-split PRs, validate the smallest useful thing. Typical validation for the first waves: - `bun typecheck` in the touched package directory or directories -- the relevant route test, especially `test/server/question-httpapi.test.ts` +- the relevant server / route coverage for the migrated slice - merged OpenAPI coverage if the PR touches spec generation Do not run tests from repo root. diff --git a/packages/opencode/specs/effect/tools.md b/packages/opencode/specs/effect/tools.md index e97e0d23e0..7b47831709 100644 --- a/packages/opencode/specs/effect/tools.md +++ b/packages/opencode/specs/effect/tools.md @@ -36,7 +36,7 @@ This keeps tool tests aligned with the production service graph and makes follow ## Exported tools -These exported tool definitions already exist in `src/tool` and are on the current Effect-native `Tool.define(...)` path: +These exported tool definitions currently use `Tool.define(...)` in `src/tool`: - [x] `apply_patch.ts` - [x] `bash.ts` @@ -45,7 +45,6 @@ These exported tool definitions already exist in `src/tool` and are on the curre - [x] `glob.ts` - [x] `grep.ts` - [x] `invalid.ts` -- [x] `ls.ts` - [x] `lsp.ts` - [x] `multiedit.ts` - [x] `plan.ts` @@ -60,7 +59,7 @@ These exported tool definitions already exist in `src/tool` and are on the curre Notes: -- `batch.ts` is no longer a current tool file and should not be tracked here. +- There is no current `ls.ts` tool file on this branch. - `truncate.ts` is an Effect service used by tools, not a tool definition itself. - `mcp-exa.ts`, `external-directory.ts`, and `schema.ts` are support modules, not standalone tool definitions. @@ -73,7 +72,7 @@ Current spot cleanups worth tracking: - [ ] `read.ts` — still bridges to Node stream / `readline` helpers and Promise-based binary detection - [ ] `bash.ts` — already uses Effect child-process primitives; only keep tracking shell-specific platform bridges and parser/loading details as they come up - [ ] `webfetch.ts` — already uses `HttpClient`; remaining work is limited to smaller boundary helpers like HTML text extraction -- [ ] `file/ripgrep.ts` — adjacent to tool migration; still has raw fs/process usage that affects `grep.ts` and `ls.ts` +- [ ] `file/ripgrep.ts` — adjacent to tool migration; still has raw fs/process usage that affects `grep.ts` and file-search routes - [ ] `patch/index.ts` — adjacent to tool migration; still has raw fs usage behind patch application Notable items that are already effectively on the target path and do not need separate migration bullets right now: @@ -83,7 +82,6 @@ Notable items that are already effectively on the target path and do not need se - `write.ts` - `codesearch.ts` - `websearch.ts` -- `ls.ts` - `multiedit.ts` - `edit.ts` From 05cdb7c1071a2ba900ba14449085d261755ff3e0 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 20:10:20 -0400 Subject: [PATCH 154/335] refactor(v2): tag session unions and exhaustively match events (#23201) --- packages/opencode/src/v2/session-entry.ts | 159 +++++++++--------- packages/opencode/src/v2/session-event.ts | 2 +- .../test/session/session-entry.test.ts | 61 ++++++- 3 files changed, 140 insertions(+), 82 deletions(-) diff --git a/packages/opencode/src/v2/session-entry.ts b/packages/opencode/src/v2/session-entry.ts index 140fa47d23..08122428ae 100644 --- a/packages/opencode/src/v2/session-entry.ts +++ b/packages/opencode/src/v2/session-entry.ts @@ -1,6 +1,6 @@ import { Schema } from "effect" import { SessionEvent } from "./session-event" -import { produce } from "immer" +import { castDraft, produce } from "immer" export const ID = SessionEvent.ID export type ID = Schema.Schema.Type @@ -70,7 +70,9 @@ export class ToolStateError extends Schema.Class("Session.Entry. metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), }) {} -export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]) +export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe( + Schema.toTaggedUnion("status"), +) export type ToolState = Schema.Schema.Type export class AssistantTool extends Schema.Class("Session.Entry.Assistant.Tool")({ @@ -96,7 +98,9 @@ export class AssistantReasoning extends Schema.Class("Sessio text: Schema.String, }) {} -export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]) +export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]).pipe( + Schema.toTaggedUnion("type"), +) export type AssistantContent = Schema.Schema.Type export class Assistant extends Schema.Class("Session.Entry.Assistant")({ @@ -126,7 +130,7 @@ export class Compaction extends Schema.Class("Session.Entry.Compacti ...Base, }) {} -export const Entry = Schema.Union([User, Synthetic, Assistant, Compaction]) +export const Entry = Schema.Union([User, Synthetic, Assistant, Compaction]).pipe(Schema.toTaggedUnion("type")) export type Entry = Schema.Schema.Type @@ -141,19 +145,29 @@ export function step(old: History, event: SessionEvent.Event): History { return produce(old, (draft) => { const lastAssistant = draft.entries.findLast((x) => x.type === "assistant") const pendingAssistant = lastAssistant && !lastAssistant.time.completed ? lastAssistant : undefined + type DraftContent = NonNullable["content"][number] + type DraftTool = Extract - switch (event.type) { - case "prompt": { + const latestTool = (callID?: string) => + pendingAssistant?.content.findLast( + (item): item is DraftTool => item.type === "tool" && (callID === undefined || item.callID === callID), + ) + const latestText = () => pendingAssistant?.content.findLast((item) => item.type === "text") + const latestReasoning = () => pendingAssistant?.content.findLast((item) => item.type === "reasoning") + + SessionEvent.Event.match(event, { + prompt: (event) => { + const entry = User.fromEvent(event) if (pendingAssistant) { - // @ts-expect-error - draft.pending.push(User.fromEvent(event)) - break + draft.pending.push(castDraft(entry)) + return } - // @ts-expect-error - draft.entries.push(User.fromEvent(event)) - break - } - case "step.started": { + draft.entries.push(castDraft(entry)) + }, + synthetic: (event) => { + draft.entries.push(new Synthetic({ ...event, time: { created: event.timestamp } })) + }, + "step.started": (event) => { if (pendingAssistant) pendingAssistant.time.completed = event.timestamp draft.entries.push({ id: event.id, @@ -163,27 +177,28 @@ export function step(old: History, event: SessionEvent.Event): History { }, content: [], }) - break - } - case "text.started": { - if (!pendingAssistant) break + }, + "step.ended": (event) => { + if (!pendingAssistant) return + pendingAssistant.time.completed = event.timestamp + pendingAssistant.cost = event.cost + pendingAssistant.tokens = event.tokens + }, + "text.started": () => { + if (!pendingAssistant) return pendingAssistant.content.push({ type: "text", text: "", }) - break - } - case "text.delta": { - if (!pendingAssistant) break - const match = pendingAssistant.content.findLast((x) => x.type === "text") + }, + "text.delta": (event) => { + if (!pendingAssistant) return + const match = latestText() if (match) match.text += event.delta - break - } - case "text.ended": { - break - } - case "tool.input.started": { - if (!pendingAssistant) break + }, + "text.ended": () => {}, + "tool.input.started": (event) => { + if (!pendingAssistant) return pendingAssistant.content.push({ type: "tool", callID: event.callID, @@ -196,21 +211,17 @@ export function step(old: History, event: SessionEvent.Event): History { input: "", }, }) - break - } - case "tool.input.delta": { - if (!pendingAssistant) break - const match = pendingAssistant.content.findLast((x) => x.type === "tool") + }, + "tool.input.delta": (event) => { + if (!pendingAssistant) return + const match = latestTool(event.callID) // oxlint-disable-next-line no-base-to-string -- event.delta is a Schema.String (runtime string) if (match) match.state.input += event.delta - break - } - case "tool.input.ended": { - break - } - case "tool.called": { - if (!pendingAssistant) break - const match = pendingAssistant.content.findLast((x) => x.type === "tool") + }, + "tool.input.ended": () => {}, + "tool.called": (event) => { + if (!pendingAssistant) return + const match = latestTool(event.callID) if (match) { match.time.ran = event.timestamp match.state = { @@ -218,11 +229,10 @@ export function step(old: History, event: SessionEvent.Event): History { input: event.input, } } - break - } - case "tool.success": { - if (!pendingAssistant) break - const match = pendingAssistant.content.findLast((x) => x.type === "tool") + }, + "tool.success": (event) => { + if (!pendingAssistant) return + const match = latestTool(event.callID) if (match && match.state.status === "running") { match.state = { status: "completed", @@ -230,15 +240,13 @@ export function step(old: History, event: SessionEvent.Event): History { output: event.output ?? "", title: event.title, metadata: event.metadata ?? {}, - // @ts-expect-error - attachments: event.attachments ?? [], + attachments: [...(event.attachments ?? [])], } } - break - } - case "tool.error": { - if (!pendingAssistant) break - const match = pendingAssistant.content.findLast((x) => x.type === "tool") + }, + "tool.error": (event) => { + if (!pendingAssistant) return + const match = latestTool(event.callID) if (match && match.state.status === "running") { match.state = { status: "error", @@ -247,36 +255,29 @@ export function step(old: History, event: SessionEvent.Event): History { metadata: event.metadata ?? {}, } } - break - } - case "reasoning.started": { - if (!pendingAssistant) break + }, + "reasoning.started": () => { + if (!pendingAssistant) return pendingAssistant.content.push({ type: "reasoning", text: "", }) - break - } - case "reasoning.delta": { - if (!pendingAssistant) break - const match = pendingAssistant.content.findLast((x) => x.type === "reasoning") + }, + "reasoning.delta": (event) => { + if (!pendingAssistant) return + const match = latestReasoning() if (match) match.text += event.delta - break - } - case "reasoning.ended": { - if (!pendingAssistant) break - const match = pendingAssistant.content.findLast((x) => x.type === "reasoning") + }, + "reasoning.ended": (event) => { + if (!pendingAssistant) return + const match = latestReasoning() if (match) match.text = event.text - break - } - case "step.ended": { - if (!pendingAssistant) break - pendingAssistant.time.completed = event.timestamp - pendingAssistant.cost = event.cost - pendingAssistant.tokens = event.tokens - break - } - } + }, + retried: () => {}, + compacted: (event) => { + draft.entries.push(new Compaction({ ...event, type: "compaction", time: { created: event.timestamp } })) + }, + }) }) } diff --git a/packages/opencode/src/v2/session-event.ts b/packages/opencode/src/v2/session-event.ts index 8ea239033f..11d4a5db2d 100644 --- a/packages/opencode/src/v2/session-event.ts +++ b/packages/opencode/src/v2/session-event.ts @@ -441,7 +441,7 @@ export namespace SessionEvent { { mode: "oneOf", }, - ) + ).pipe(Schema.toTaggedUnion("type")) export type Event = Schema.Schema.Type export type Type = Event["type"] } diff --git a/packages/opencode/test/session/session-entry.test.ts b/packages/opencode/test/session/session-entry.test.ts index 7eba3900d7..dea8da20a0 100644 --- a/packages/opencode/test/session/session-entry.test.ts +++ b/packages/opencode/test/session/session-entry.test.ts @@ -591,7 +591,64 @@ describe("session-entry step", () => { ) }) - test.failing("records synthetic events", () => { + test("routes tool events by callID when tool streams interleave", () => { + FastCheck.assert( + FastCheck.property(dict, dict, word, word, text, text, (a, b, titleA, titleB, deltaA, deltaB) => { + const next = run( + [ + SessionEvent.Tool.Input.Started.create({ callID: "a", name: "bash", timestamp: time(1) }), + SessionEvent.Tool.Input.Started.create({ callID: "b", name: "grep", timestamp: time(2) }), + SessionEvent.Tool.Input.Delta.create({ callID: "a", delta: deltaA, timestamp: time(3) }), + SessionEvent.Tool.Input.Delta.create({ callID: "b", delta: deltaB, timestamp: time(4) }), + SessionEvent.Tool.Called.create({ + callID: "a", + tool: "bash", + input: a, + provider: { executed: true }, + timestamp: time(5), + }), + SessionEvent.Tool.Called.create({ + callID: "b", + tool: "grep", + input: b, + provider: { executed: true }, + timestamp: time(6), + }), + SessionEvent.Tool.Success.create({ + callID: "a", + title: titleA, + output: "done-a", + provider: { executed: true }, + timestamp: time(7), + }), + SessionEvent.Tool.Success.create({ + callID: "b", + title: titleB, + output: "done-b", + provider: { executed: true }, + timestamp: time(8), + }), + ], + active(), + ) + + const first = tool(next, "a") + const second = tool(next, "b") + + expect(first?.state.status).toBe("completed") + expect(second?.state.status).toBe("completed") + if (first?.state.status !== "completed" || second?.state.status !== "completed") return + + expect(first.state.input).toEqual(a) + expect(second.state.input).toEqual(b) + expect(first.state.title).toBe(titleA) + expect(second.state.title).toBe(titleB) + }), + { numRuns: 50 }, + ) + }) + + test("records synthetic events", () => { FastCheck.assert( FastCheck.property(word, (body) => { const next = SessionEntry.step(history(), SessionEvent.Synthetic.create({ text: body, timestamp: time(1) })) @@ -604,7 +661,7 @@ describe("session-entry step", () => { ) }) - test.failing("records compaction events", () => { + test("records compaction events", () => { FastCheck.assert( FastCheck.property(FastCheck.boolean(), maybe(FastCheck.boolean()), (auto, overflow) => { const next = SessionEntry.step( From bb90f3bbf99e00c2b5d780b38da318cfa3fd4c72 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 20:50:36 -0400 Subject: [PATCH 155/335] feat(effect-zod): translate well-known filters into native Zod methods (#23209) --- packages/opencode/src/util/effect-zod.ts | 89 +++++++- .../opencode/test/util/effect-zod.test.ts | 191 ++++++++++++++++++ 2 files changed, 275 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/util/effect-zod.ts b/packages/opencode/src/util/effect-zod.ts index 82c661e402..227a708442 100644 --- a/packages/opencode/src/util/effect-zod.ts +++ b/packages/opencode/src/util/effect-zod.ts @@ -75,9 +75,12 @@ function decode(transformation: SchemaAST.Link["transformation"], value: unknown return Option.getOrElse(exit.value, () => value) } -// Flatten FilterGroups and any nested variants into a linear list of Filters -// so we can run all of them inside a single Zod .superRefine wrapper instead -// of stacking N wrapper layers (one per check). +// Flatten FilterGroups and any nested variants into a linear list of Filters. +// Well-known filters (Schema.isInt, isGreaterThan, isPattern, …) are +// translated into native Zod methods so their JSON Schema output includes +// the corresponding constraint (type: integer, exclusiveMinimum, pattern, …). +// Anything else falls back to a single .superRefine layer — runtime-only, +// emits no JSON Schema constraint. function applyChecks(out: z.ZodTypeAny, checks: SchemaAST.Checks, ast: SchemaAST.AST): z.ZodTypeAny { const filters: SchemaAST.Filter[] = [] const collect = (c: SchemaAST.Check) => { @@ -85,8 +88,19 @@ function applyChecks(out: z.ZodTypeAny, checks: SchemaAST.Checks, ast: SchemaAST else filters.push(c) } checks.forEach(collect) - return out.superRefine((value, ctx) => { - for (const filter of filters) { + + const unhandled: SchemaAST.Filter[] = [] + const translated = filters.reduce((acc, filter) => { + const next = translateFilter(acc, filter) + if (next) return next + unhandled.push(filter) + return acc + }, out) + + if (unhandled.length === 0) return translated + + return translated.superRefine((value, ctx) => { + for (const filter of unhandled) { const issue = filter.run(value, ast, EMPTY_PARSE_OPTIONS) if (!issue) continue const message = issueMessage(issue) ?? (filter.annotations as any)?.message ?? "Validation failed" @@ -95,6 +109,71 @@ function applyChecks(out: z.ZodTypeAny, checks: SchemaAST.Checks, ast: SchemaAST }) } +// Translate a well-known Effect Schema filter into a native Zod method call on +// `out`. Dispatch is keyed on `filter.annotations.meta._tag`, which every +// built-in check factory (isInt, isGreaterThan, isPattern, …) attaches at +// construction time. Returns `undefined` for unrecognised filters so the +// caller can fall back to the generic .superRefine path. +function translateFilter(out: z.ZodTypeAny, filter: SchemaAST.Filter): z.ZodTypeAny | undefined { + const meta = (filter.annotations as { meta?: Record } | undefined)?.meta + if (!meta || typeof meta._tag !== "string") return undefined + switch (meta._tag) { + case "isInt": + return call(out, "int") + case "isFinite": + return call(out, "finite") + case "isGreaterThan": + return call(out, "gt", meta.exclusiveMinimum) + case "isGreaterThanOrEqualTo": + return call(out, "gte", meta.minimum) + case "isLessThan": + return call(out, "lt", meta.exclusiveMaximum) + case "isLessThanOrEqualTo": + return call(out, "lte", meta.maximum) + case "isBetween": { + const lo = meta.exclusiveMinimum ? call(out, "gt", meta.minimum) : call(out, "gte", meta.minimum) + if (!lo) return undefined + return meta.exclusiveMaximum ? call(lo, "lt", meta.maximum) : call(lo, "lte", meta.maximum) + } + case "isMultipleOf": + return call(out, "multipleOf", meta.divisor) + case "isMinLength": + return call(out, "min", meta.minLength) + case "isMaxLength": + return call(out, "max", meta.maxLength) + case "isLengthBetween": { + const lo = call(out, "min", meta.minimum) + if (!lo) return undefined + return call(lo, "max", meta.maximum) + } + case "isPattern": + return call(out, "regex", meta.regExp) + case "isStartsWith": + return call(out, "startsWith", meta.startsWith) + case "isEndsWith": + return call(out, "endsWith", meta.endsWith) + case "isIncludes": + return call(out, "includes", meta.includes) + case "isUUID": + return call(out, "uuid") + case "isULID": + return call(out, "ulid") + case "isBase64": + return call(out, "base64") + case "isBase64Url": + return call(out, "base64url") + } + return undefined +} + +// Invoke a named Zod method on `target` if it exists, otherwise return +// undefined so the caller can fall back. Using this helper instead of a +// typed cast keeps `translateFilter` free of per-case narrowing noise. +function call(target: z.ZodTypeAny, method: string, ...args: unknown[]): z.ZodTypeAny | undefined { + const fn = (target as unknown as Record z.ZodTypeAny) | undefined>)[method] + return typeof fn === "function" ? fn.apply(target, args) : undefined +} + function issueMessage(issue: any): string | undefined { if (typeof issue?.annotations?.message === "string") return issue.annotations.message if (typeof issue?.message === "string") return issue.message diff --git a/packages/opencode/test/util/effect-zod.test.ts b/packages/opencode/test/util/effect-zod.test.ts index 3d72984bfc..1d999e979d 100644 --- a/packages/opencode/test/util/effect-zod.test.ts +++ b/packages/opencode/test/util/effect-zod.test.ts @@ -478,4 +478,195 @@ describe("util.effect-zod", () => { expect(bad.error!.issues.map((i) => i.message)).toEqual(expect.arrayContaining(["not positive", "not even"])) }) }) + + describe("well-known refinement translation", () => { + test("Schema.isInt emits type: integer in JSON Schema", () => { + const schema = zod(Schema.Number.check(Schema.isInt())) + const native = json(z.number().int()) + expect(json(schema)).toEqual(native) + expect(schema.parse(3)).toBe(3) + expect(schema.safeParse(1.5).success).toBe(false) + }) + + test("Schema.isGreaterThan(0) emits exclusiveMinimum: 0", () => { + const schema = zod(Schema.Number.check(Schema.isGreaterThan(0))) + expect((json(schema) as any).exclusiveMinimum).toBe(0) + expect(schema.parse(1)).toBe(1) + expect(schema.safeParse(0).success).toBe(false) + expect(schema.safeParse(-1).success).toBe(false) + }) + + test("Schema.isGreaterThanOrEqualTo(0) emits minimum: 0", () => { + const schema = zod(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))) + expect((json(schema) as any).minimum).toBe(0) + expect(schema.parse(0)).toBe(0) + expect(schema.safeParse(-1).success).toBe(false) + }) + + test("Schema.isLessThan(10) emits exclusiveMaximum: 10", () => { + const schema = zod(Schema.Number.check(Schema.isLessThan(10))) + expect((json(schema) as any).exclusiveMaximum).toBe(10) + expect(schema.parse(9)).toBe(9) + expect(schema.safeParse(10).success).toBe(false) + }) + + test("Schema.isLessThanOrEqualTo(10) emits maximum: 10", () => { + const schema = zod(Schema.Number.check(Schema.isLessThanOrEqualTo(10))) + expect((json(schema) as any).maximum).toBe(10) + expect(schema.parse(10)).toBe(10) + expect(schema.safeParse(11).success).toBe(false) + }) + + test("Schema.isMultipleOf(5) emits multipleOf: 5", () => { + const schema = zod(Schema.Number.check(Schema.isMultipleOf(5))) + expect((json(schema) as any).multipleOf).toBe(5) + expect(schema.parse(10)).toBe(10) + expect(schema.safeParse(7).success).toBe(false) + }) + + test("Schema.isFinite validates at runtime", () => { + const schema = zod(Schema.Number.check(Schema.isFinite())) + expect(schema.parse(1)).toBe(1) + expect(schema.safeParse(Infinity).success).toBe(false) + expect(schema.safeParse(NaN).success).toBe(false) + }) + + test("chained isInt + isGreaterThan(0) matches z.number().int().positive()", () => { + const schema = zod(Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))) + const native = json(z.number().int().positive()) + expect(json(schema)).toEqual(native) + expect(schema.parse(3)).toBe(3) + expect(schema.safeParse(0).success).toBe(false) + expect(schema.safeParse(1.5).success).toBe(false) + }) + + test("chained isInt + isGreaterThanOrEqualTo(0) matches z.number().int().min(0)", () => { + const schema = zod(Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))) + const native = json(z.number().int().min(0)) + expect(json(schema)).toEqual(native) + expect(schema.parse(0)).toBe(0) + expect(schema.safeParse(-1).success).toBe(false) + }) + + test("Schema.isBetween emits both bounds", () => { + const schema = zod(Schema.Number.check(Schema.isBetween({ minimum: 1, maximum: 10 }))) + const shape = json(schema) as any + expect(shape.minimum).toBe(1) + expect(shape.maximum).toBe(10) + expect(schema.parse(5)).toBe(5) + expect(schema.safeParse(11).success).toBe(false) + expect(schema.safeParse(0).success).toBe(false) + }) + + test("Schema.isBetween with exclusive bounds emits exclusiveMinimum/Maximum", () => { + const schema = zod( + Schema.Number.check( + Schema.isBetween({ minimum: 1, maximum: 10, exclusiveMinimum: true, exclusiveMaximum: true }), + ), + ) + const shape = json(schema) as any + expect(shape.exclusiveMinimum).toBe(1) + expect(shape.exclusiveMaximum).toBe(10) + expect(schema.parse(5)).toBe(5) + expect(schema.safeParse(1).success).toBe(false) + expect(schema.safeParse(10).success).toBe(false) + }) + + test("Schema.isInt32 (FilterGroup) produces integer bounds", () => { + const schema = zod(Schema.Number.check(Schema.isInt32())) + const shape = json(schema) as any + expect(shape.type).toBe("integer") + expect(shape.minimum).toBe(-2147483648) + expect(shape.maximum).toBe(2147483647) + expect(schema.parse(42)).toBe(42) + expect(schema.safeParse(1.5).success).toBe(false) + expect(schema.safeParse(2147483648).success).toBe(false) + }) + + test("Schema.isMinLength on string emits minLength", () => { + const schema = zod(Schema.String.check(Schema.isMinLength(3))) + expect((json(schema) as any).minLength).toBe(3) + expect(schema.parse("abc")).toBe("abc") + expect(schema.safeParse("ab").success).toBe(false) + }) + + test("Schema.isMaxLength on string emits maxLength", () => { + const schema = zod(Schema.String.check(Schema.isMaxLength(5))) + expect((json(schema) as any).maxLength).toBe(5) + expect(schema.parse("abcde")).toBe("abcde") + expect(schema.safeParse("abcdef").success).toBe(false) + }) + + test("Schema.isLengthBetween on string emits both bounds", () => { + const schema = zod(Schema.String.check(Schema.isLengthBetween(2, 4))) + const shape = json(schema) as any + expect(shape.minLength).toBe(2) + expect(shape.maxLength).toBe(4) + expect(schema.parse("abc")).toBe("abc") + expect(schema.safeParse("a").success).toBe(false) + expect(schema.safeParse("abcde").success).toBe(false) + }) + + test("Schema.isMinLength on array emits minItems", () => { + const schema = zod(Schema.Array(Schema.String).check(Schema.isMinLength(1))) + expect((json(schema) as any).minItems).toBe(1) + expect(schema.parse(["x"])).toEqual(["x"]) + expect(schema.safeParse([]).success).toBe(false) + }) + + test("Schema.isPattern emits pattern", () => { + const schema = zod(Schema.String.check(Schema.isPattern(/^per/))) + expect((json(schema) as any).pattern).toBe("^per") + expect(schema.parse("per_abc")).toBe("per_abc") + expect(schema.safeParse("abc").success).toBe(false) + }) + + test("Schema.isStartsWith matches native zod .startsWith() JSON Schema", () => { + const schema = zod(Schema.String.check(Schema.isStartsWith("per"))) + const native = json(z.string().startsWith("per")) + expect(json(schema)).toEqual(native) + expect(schema.parse("per_abc")).toBe("per_abc") + expect(schema.safeParse("abc").success).toBe(false) + }) + + test("Schema.isEndsWith matches native zod .endsWith() JSON Schema", () => { + const schema = zod(Schema.String.check(Schema.isEndsWith(".json"))) + const native = json(z.string().endsWith(".json")) + expect(json(schema)).toEqual(native) + expect(schema.parse("a.json")).toBe("a.json") + expect(schema.safeParse("a.txt").success).toBe(false) + }) + + test("Schema.isUUID emits format: uuid", () => { + const schema = zod(Schema.String.check(Schema.isUUID())) + expect((json(schema) as any).format).toBe("uuid") + }) + + test("mix of well-known and anonymous filters translates known and reroutes unknown to superRefine", () => { + // isInt is well-known (translates to .int()); the anonymous filter falls + // back to superRefine. + const notSeven = Schema.makeFilter((n: number) => (n !== 7 ? undefined : "no sevens allowed")) + const schema = zod(Schema.Number.check(Schema.isInt()).check(notSeven)) + + const shape = json(schema) as any + // Well-known translation is preserved — type is integer, not plain number + expect(shape.type).toBe("integer") + + // Runtime: both constraints fire + expect(schema.parse(3)).toBe(3) + expect(schema.safeParse(1.5).success).toBe(false) + const seven = schema.safeParse(7) + expect(seven.success).toBe(false) + expect(seven.error!.issues[0].message).toBe("no sevens allowed") + }) + + test("inside a struct field, well-known refinements propagate through", () => { + // Mirrors config.ts port: z.number().int().positive().optional() + const Port = Schema.optional(Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))) + const schema = zod(Schema.Struct({ port: Port })) + const shape = json(schema) as any + expect(shape.properties.port.type).toBe("integer") + expect(shape.properties.port.exclusiveMinimum).toBe(0) + }) + }) }) From 36119ff1731b3bc94ed0cffcb32b05987a48b088 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 20:55:38 -0400 Subject: [PATCH 156/335] feat(effect-zod): translate Schema.withDecodingDefault into zod .default() (#23207) --- packages/opencode/src/util/effect-zod.ts | 48 ++++++++-- .../opencode/test/util/effect-zod.test.ts | 87 ++++++++++++++++++- 2 files changed, 129 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/util/effect-zod.ts b/packages/opencode/src/util/effect-zod.ts index 227a708442..cdcd99c976 100644 --- a/packages/opencode/src/util/effect-zod.ts +++ b/packages/opencode/src/util/effect-zod.ts @@ -40,7 +40,12 @@ function walkUncached(ast: SchemaAST.AST): z.ZodTypeAny { // Declarations fall through to body(), not encoded(). User-level // Schema.decodeTo / Schema.transform attach encoding to non-Declaration // nodes, where we do apply the transform. - const hasTransform = ast.encoding?.length && ast._tag !== "Declaration" + // + // Schema.withDecodingDefault also attaches encoding, but we want `.default(v)` + // on the inner Zod rather than a transform wrapper — so optional ASTs whose + // encoding resolves a default from Option.none() route through body()/opt(). + const hasEncoding = ast.encoding?.length && ast._tag !== "Declaration" + const hasTransform = hasEncoding && !(SchemaAST.isOptional(ast) && extractDefault(ast) !== undefined) const base = hasTransform ? encoded(ast) : body(ast) const out = ast.checks?.length ? applyChecks(base, ast.checks, ast) : base const desc = SchemaAST.resolveDescription(ast) @@ -217,10 +222,43 @@ function body(ast: SchemaAST.AST): z.ZodTypeAny { function opt(ast: SchemaAST.AST): z.ZodTypeAny { if (ast._tag !== "Union") return fail(ast) const items = ast.types.filter((item) => item._tag !== "Undefined") - if (items.length === 1) return walk(items[0]).optional() - if (items.length > 1) - return z.union(items.map(walk) as [z.ZodTypeAny, z.ZodTypeAny, ...Array]).optional() - return z.undefined().optional() + const inner = + items.length === 1 + ? walk(items[0]) + : items.length > 1 + ? z.union(items.map(walk) as [z.ZodTypeAny, z.ZodTypeAny, ...Array]) + : z.undefined() + // Schema.withDecodingDefault attaches an encoding `Link` whose transformation + // decode Getter resolves `Option.none()` to `Option.some(default)`. Invoke + // it to extract the default and emit `.default(...)` instead of `.optional()`. + const fallback = extractDefault(ast) + if (fallback !== undefined) return inner.default(fallback.value) + return inner.optional() +} + +type DecodeLink = { + readonly transformation: { + readonly decode: { + readonly run: ( + input: Option.Option, + options: SchemaAST.ParseOptions, + ) => Effect.Effect, unknown> + } + } +} + +function extractDefault(ast: SchemaAST.AST): { value: unknown } | undefined { + const encoding = (ast as { encoding?: ReadonlyArray }).encoding + if (!encoding?.length) return undefined + // Walk the chain of encoding Links in order; the first Getter that produces + // a value from Option.none wins. withDecodingDefault always puts its + // defaulting Link adjacent to the optional Union. + for (const link of encoding) { + const probe = Effect.runSyncExit(link.transformation.decode.run(Option.none(), {})) + if (probe._tag !== "Success") continue + if (Option.isSome(probe.value)) return { value: probe.value.value } + } + return undefined } function union(ast: SchemaAST.Union): z.ZodTypeAny { diff --git a/packages/opencode/test/util/effect-zod.test.ts b/packages/opencode/test/util/effect-zod.test.ts index 1d999e979d..7ce43af5fb 100644 --- a/packages/opencode/test/util/effect-zod.test.ts +++ b/packages/opencode/test/util/effect-zod.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { Schema, SchemaGetter } from "effect" +import { Effect, Schema, SchemaGetter } from "effect" import z from "zod" import { zod, ZodOverride } from "../../src/util/effect-zod" @@ -669,4 +669,89 @@ describe("util.effect-zod", () => { expect(shape.properties.port.exclusiveMinimum).toBe(0) }) }) + + describe("Schema.optionalWith defaults", () => { + test("parsing undefined returns the default value", () => { + const schema = zod( + Schema.Struct({ + mode: Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed("ctrl-x"))), + }), + ) + expect(schema.parse({})).toEqual({ mode: "ctrl-x" }) + expect(schema.parse({ mode: undefined })).toEqual({ mode: "ctrl-x" }) + }) + + test("parsing a real value returns that value (default does not fire)", () => { + const schema = zod( + Schema.Struct({ + mode: Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed("ctrl-x"))), + }), + ) + expect(schema.parse({ mode: "ctrl-y" })).toEqual({ mode: "ctrl-y" }) + }) + + test("default on a number field", () => { + const schema = zod( + Schema.Struct({ + count: Schema.Number.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(42))), + }), + ) + expect(schema.parse({})).toEqual({ count: 42 }) + expect(schema.parse({ count: 7 })).toEqual({ count: 7 }) + }) + + test("multiple defaulted fields inside a struct", () => { + const schema = zod( + Schema.Struct({ + leader: Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed("ctrl-x"))), + quit: Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed("ctrl-c"))), + inner: Schema.String, + }), + ) + expect(schema.parse({ inner: "hi" })).toEqual({ + leader: "ctrl-x", + quit: "ctrl-c", + inner: "hi", + }) + expect(schema.parse({ leader: "a", quit: "b", inner: "c" })).toEqual({ + leader: "a", + quit: "b", + inner: "c", + }) + }) + + test("JSON Schema output includes the default key", () => { + const schema = zod( + Schema.Struct({ + mode: Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed("ctrl-x"))), + }), + ) + const shape = json(schema) as any + expect(shape.properties.mode.default).toBe("ctrl-x") + }) + + test("default referencing a computed value resolves when evaluated", () => { + // Simulates `keybinds.ts` style of per-platform defaults: the default is + // produced by an Effect that computes a value at decode time. + const platform = "darwin" + const fallback = platform === "darwin" ? "cmd-k" : "ctrl-k" + const schema = zod( + Schema.Struct({ + command_palette: Schema.String.pipe( + Schema.optional, + Schema.withDecodingDefault(Effect.sync(() => fallback)), + ), + }), + ) + expect(schema.parse({})).toEqual({ command_palette: "cmd-k" }) + const shape = json(schema) as any + expect(shape.properties.command_palette.default).toBe("cmd-k") + }) + + test("plain Schema.optional (no default) still emits .optional() (regression)", () => { + const schema = zod(Schema.Struct({ foo: Schema.optional(Schema.String) })) + expect(schema.parse({})).toEqual({}) + expect(schema.parse({ foo: "hi" })).toEqual({ foo: "hi" }) + }) + }) }) From f52ae28432f66eef13ea48203899b23992f89eb9 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 18 Apr 2026 00:56:33 +0000 Subject: [PATCH 157/335] chore: generate --- packages/opencode/test/util/effect-zod.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/opencode/test/util/effect-zod.test.ts b/packages/opencode/test/util/effect-zod.test.ts index 7ce43af5fb..70cd8f0e64 100644 --- a/packages/opencode/test/util/effect-zod.test.ts +++ b/packages/opencode/test/util/effect-zod.test.ts @@ -737,10 +737,7 @@ describe("util.effect-zod", () => { const fallback = platform === "darwin" ? "cmd-k" : "ctrl-k" const schema = zod( Schema.Struct({ - command_palette: Schema.String.pipe( - Schema.optional, - Schema.withDecodingDefault(Effect.sync(() => fallback)), - ), + command_palette: Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.sync(() => fallback))), }), ) expect(schema.parse({})).toEqual({ command_palette: "cmd-k" }) From 5181f9b4e141fc6c0c3c3d26169721af9e63ca51 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 21:04:40 -0400 Subject: [PATCH 158/335] refactor(config): drop ZodOverride from PositiveInt in provider.ts (#23215) --- packages/opencode/src/config/provider.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/config/provider.ts b/packages/opencode/src/config/provider.ts index b435f43759..4b08592a65 100644 --- a/packages/opencode/src/config/provider.ts +++ b/packages/opencode/src/config/provider.ts @@ -1,12 +1,10 @@ import { Schema } from "effect" -import z from "zod" -import { zod, ZodOverride } from "@/util/effect-zod" +import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" -// Positive integer preserving exact Zod JSON Schema (type: integer, exclusiveMinimum: 0). -const PositiveInt = Schema.Number.annotate({ - [ZodOverride]: z.number().int().positive(), -}) +// Positive integer: emits JSON Schema `type: integer, exclusiveMinimum: 0` +// via the effect-zod walker's well-known refinement translation. +const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)) export const Model = Schema.Struct({ id: Schema.optional(Schema.String), From 23a2d0128254480e51c630f8cd077af29dc39efa Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 21:14:23 -0400 Subject: [PATCH 159/335] fix(observability): standardize session telemetry attrs (#23213) --- packages/opencode/src/effect/logger.ts | 8 +++++++- packages/opencode/src/session/llm.ts | 16 ++++++++++++++-- packages/opencode/src/session/processor.ts | 2 +- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/effect/logger.ts b/packages/opencode/src/effect/logger.ts index 21e0fc43ac..0e58b8acb4 100644 --- a/packages/opencode/src/effect/logger.ts +++ b/packages/opencode/src/effect/logger.ts @@ -3,6 +3,8 @@ import { Log } from "@/util" type Fields = Record +const normalizeKey = (key: string) => (key === "sessionID" ? "session.id" : key) + export interface Handle { readonly debug: (msg?: unknown, extra?: Fields) => Effect.Effect readonly info: (msg?: unknown, extra?: Fields) => Effect.Effect @@ -12,7 +14,11 @@ export interface Handle { } const clean = (input?: Fields): Fields => - Object.fromEntries(Object.entries(input ?? {}).filter((entry) => entry[1] !== undefined && entry[1] !== null)) + Object.fromEntries( + Object.entries(input ?? {}) + .filter((entry) => entry[1] !== undefined && entry[1] !== null) + .map(([key, value]) => [normalizeKey(key), value]), + ) const text = (input: unknown): string => { // oxlint-disable-next-line no-base-to-string diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index b66e99fc82..b72f873de0 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -74,7 +74,7 @@ const live: Layer.Layer< .clone() .tag("providerID", input.model.providerID) .tag("modelID", input.model.id) - .tag("sessionID", input.sessionID) + .tag("session.id", input.sessionID) .tag("small", (input.small ?? false).toString()) .tag("agent", input.agent.name) .tag("mode", input.agent.mode) @@ -317,6 +317,18 @@ const live: Layer.Layer< const tracer = cfg.experimental?.openTelemetry ? Option.getOrUndefined(yield* Effect.serviceOption(OtelTracer.OtelTracer)) : undefined + const telemetryTracer = tracer + ? new Proxy(tracer, { + get(target, prop, receiver) { + if (prop !== "startSpan") return Reflect.get(target, prop, receiver) + return (...args: Parameters) => { + const span = target.startSpan(...args) + span.setAttribute("session.id", input.sessionID) + return span + } + }, + }) + : undefined return streamText({ onError(error) { @@ -390,7 +402,7 @@ const live: Layer.Layer< experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry, functionId: "session.llm", - tracer, + tracer: telemetryTracer, metadata: { userId: cfg.username ?? "unknown", sessionId: input.sessionID, diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 9ab74ca341..21f9329c6f 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -123,7 +123,7 @@ export const layer: Layer.Layer< reasoningMap: {}, } let aborted = false - const slog = log.clone().tag("sessionID", input.sessionID).tag("messageID", input.assistantMessage.id) + const slog = log.clone().tag("session.id", input.sessionID).tag("messageID", input.assistantMessage.id) const parse = (e: unknown) => MessageV2.fromError(e, { From 826fd3350ce8aa1a855956f956038fad7ecffb86 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 21:20:06 -0400 Subject: [PATCH 160/335] refactor(config): migrate Server + Layout to Effect Schema (#23216) --- packages/opencode/src/config/config.ts | 38 ++++++++---------------- packages/opencode/src/config/layout.ts | 10 +++++++ packages/opencode/src/config/provider.ts | 2 -- packages/opencode/src/config/server.ts | 20 +++++++++++++ 4 files changed, 43 insertions(+), 27 deletions(-) create mode 100644 packages/opencode/src/config/layout.ts create mode 100644 packages/opencode/src/config/server.ts diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index bfb0c2f1f4..179c6a6093 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -25,18 +25,20 @@ import { Context, Duration, Effect, Exit, Fiber, Layer, Option } from "effect" import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" import { InstanceRef } from "@/effect/instance-ref" import { ConfigAgent } from "./agent" +import { ConfigCommand } from "./command" +import { ConfigFormatter } from "./formatter" +import { ConfigLayout } from "./layout" +import { ConfigLSP } from "./lsp" +import { ConfigManaged } from "./managed" import { ConfigMCP } from "./mcp" import { ConfigModelID } from "./model-id" -import { ConfigPlugin } from "./plugin" -import { ConfigManaged } from "./managed" -import { ConfigCommand } from "./command" import { ConfigParse } from "./parse" -import { ConfigPermission } from "./permission" -import { ConfigProvider } from "./provider" -import { ConfigSkills } from "./skills" import { ConfigPaths } from "./paths" -import { ConfigFormatter } from "./formatter" -import { ConfigLSP } from "./lsp" +import { ConfigPermission } from "./permission" +import { ConfigPlugin } from "./plugin" +import { ConfigProvider } from "./provider" +import { ConfigServer } from "./server" +import { ConfigSkills } from "./skills" import { ConfigVariable } from "./variable" import { Npm } from "@/npm" @@ -73,23 +75,9 @@ async function resolveLoadedPlugins( return config } -export const Server = z - .object({ - port: z.number().int().positive().optional().describe("Port to listen on"), - hostname: z.string().optional().describe("Hostname to listen on"), - mdns: z.boolean().optional().describe("Enable mDNS service discovery"), - mdnsDomain: z.string().optional().describe("Custom domain name for mDNS service (default: opencode.local)"), - cors: z.array(z.string()).optional().describe("Additional domains to allow for CORS"), - }) - .strict() - .meta({ - ref: "ServerConfig", - }) - -export const Layout = z.enum(["auto", "stretch"]).meta({ - ref: "LayoutConfig", -}) -export type Layout = z.infer +export const Server = ConfigServer.Server.zod +export const Layout = ConfigLayout.Layout.zod +export type Layout = ConfigLayout.Layout export const Info = z .object({ diff --git a/packages/opencode/src/config/layout.ts b/packages/opencode/src/config/layout.ts new file mode 100644 index 0000000000..49c34b6639 --- /dev/null +++ b/packages/opencode/src/config/layout.ts @@ -0,0 +1,10 @@ +import { Schema } from "effect" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" + +export const Layout = Schema.Literals(["auto", "stretch"]) + .annotate({ identifier: "LayoutConfig" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Layout = Schema.Schema.Type + +export * as ConfigLayout from "./layout" diff --git a/packages/opencode/src/config/provider.ts b/packages/opencode/src/config/provider.ts index 4b08592a65..212e716251 100644 --- a/packages/opencode/src/config/provider.ts +++ b/packages/opencode/src/config/provider.ts @@ -2,8 +2,6 @@ import { Schema } from "effect" import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" -// Positive integer: emits JSON Schema `type: integer, exclusiveMinimum: 0` -// via the effect-zod walker's well-known refinement translation. const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)) export const Model = Schema.Struct({ diff --git a/packages/opencode/src/config/server.ts b/packages/opencode/src/config/server.ts new file mode 100644 index 0000000000..969a79964b --- /dev/null +++ b/packages/opencode/src/config/server.ts @@ -0,0 +1,20 @@ +import { Schema } from "effect" +import { zod } from "@/util/effect-zod" + +export class Server extends Schema.Class("ServerConfig")({ + port: Schema.optional(Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))).annotate({ + description: "Port to listen on", + }), + hostname: Schema.optional(Schema.String).annotate({ description: "Hostname to listen on" }), + mdns: Schema.optional(Schema.Boolean).annotate({ description: "Enable mDNS service discovery" }), + mdnsDomain: Schema.optional(Schema.String).annotate({ + description: "Custom domain name for mDNS service (default: opencode.local)", + }), + cors: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({ + description: "Additional domains to allow for CORS", + }), +}) { + static readonly zod = zod(this) +} + +export * as ConfigServer from "./server" From a92c75e5f4286d603c44887e50f7f9adf9e56b40 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 18 Apr 2026 01:21:01 +0000 Subject: [PATCH 161/335] chore: generate --- packages/sdk/openapi.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 5a93c4db2a..b97d596b93 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -10949,8 +10949,7 @@ "type": "string" } } - }, - "additionalProperties": false + } }, "PermissionActionConfig": { "type": "string", From a5d99e7a3c484fc020ae1f9a466542332b103807 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 21:22:36 -0400 Subject: [PATCH 162/335] refactor: pass formatter instance context explicitly (#23020) --- packages/opencode/src/format/formatter.ts | 40 ++++++++++++----------- packages/opencode/src/format/index.ts | 6 ++-- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index 36249db7db..03f8365274 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -1,15 +1,17 @@ import { Npm } from "../npm" -import { Instance } from "../project/instance" +import type { InstanceContext } from "../project/instance" import { Filesystem } from "../util" import { Process } from "../util" import { which } from "../util/which" import { Flag } from "@/flag/flag" +export interface Context extends Pick {} + export interface Info { name: string environment?: Record extensions: string[] - enabled(): Promise + enabled(context: Context): Promise } export const gofmt: Info = { @@ -65,8 +67,8 @@ export const prettier: Info = { ".graphql", ".gql", ], - async enabled() { - const items = await Filesystem.findUp("package.json", Instance.directory, Instance.worktree) + async enabled(context) { + const items = await Filesystem.findUp("package.json", context.directory, context.worktree) for (const item of items) { const json = await Filesystem.readJson<{ dependencies?: Record @@ -87,9 +89,9 @@ export const oxfmt: Info = { BUN_BE_BUN: "1", }, extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"], - async enabled() { + async enabled(context) { if (!Flag.OPENCODE_EXPERIMENTAL_OXFMT) return false - const items = await Filesystem.findUp("package.json", Instance.directory, Instance.worktree) + const items = await Filesystem.findUp("package.json", context.directory, context.worktree) for (const item of items) { const json = await Filesystem.readJson<{ dependencies?: Record @@ -137,10 +139,10 @@ export const biome: Info = { ".graphql", ".gql", ], - async enabled() { + async enabled(context) { const configs = ["biome.json", "biome.jsonc"] for (const config of configs) { - const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree) + const found = await Filesystem.findUp(config, context.directory, context.worktree) if (found.length > 0) { const bin = await Npm.which("@biomejs/biome") if (bin) return [bin, "format", "--write", "$FILE"] @@ -163,8 +165,8 @@ export const zig: Info = { export const clang: Info = { name: "clang-format", extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"], - async enabled() { - const items = await Filesystem.findUp(".clang-format", Instance.directory, Instance.worktree) + async enabled(context) { + const items = await Filesystem.findUp(".clang-format", context.directory, context.worktree) if (items.length > 0) { const match = which("clang-format") if (match) return [match, "-i", "$FILE"] @@ -186,11 +188,11 @@ export const ktlint: Info = { export const ruff: Info = { name: "ruff", extensions: [".py", ".pyi"], - async enabled() { + async enabled(context) { if (!which("ruff")) return false const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"] for (const config of configs) { - const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree) + const found = await Filesystem.findUp(config, context.directory, context.worktree) if (found.length > 0) { if (config === "pyproject.toml") { const content = await Filesystem.readText(found[0]) @@ -202,7 +204,7 @@ export const ruff: Info = { } const deps = ["requirements.txt", "pyproject.toml", "Pipfile"] for (const dep of deps) { - const found = await Filesystem.findUp(dep, Instance.directory, Instance.worktree) + const found = await Filesystem.findUp(dep, context.directory, context.worktree) if (found.length > 0) { const content = await Filesystem.readText(found[0]) if (content.includes("ruff")) return ["ruff", "format", "$FILE"] @@ -233,8 +235,8 @@ export const rlang: Info = { export const uvformat: Info = { name: "uv", extensions: [".py", ".pyi"], - async enabled() { - if (await ruff.enabled()) return false + async enabled(context) { + if (await ruff.enabled(context)) return false const uv = which("uv") if (uv == null) return false const output = await Process.run([uv, "format", "--help"], { nothrow: true }) @@ -286,9 +288,9 @@ export const dart: Info = { export const ocamlformat: Info = { name: "ocamlformat", extensions: [".ml", ".mli"], - async enabled() { + async enabled(context) { if (!which("ocamlformat")) return false - const items = await Filesystem.findUp(".ocamlformat", Instance.directory, Instance.worktree) + const items = await Filesystem.findUp(".ocamlformat", context.directory, context.worktree) if (items.length > 0) return ["ocamlformat", "-i", "$FILE"] return false }, @@ -357,8 +359,8 @@ export const rustfmt: Info = { export const pint: Info = { name: "pint", extensions: [".php"], - async enabled() { - const items = await Filesystem.findUp("composer.json", Instance.directory, Instance.worktree) + async enabled(context) { + const items = await Filesystem.findUp("composer.json", context.directory, context.worktree) for (const item of items) { const json = await Filesystem.readJson<{ require?: Record diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index 2d0f80a10c..85934ce9c9 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -37,14 +37,14 @@ export const layer = Layer.effect( const spawner = yield* ChildProcessSpawner.ChildProcessSpawner const state = yield* InstanceState.make( - Effect.fn("Format.state")(function* (_ctx) { + Effect.fn("Format.state")(function* (ctx) { const commands: Record = {} const formatters: Record = {} async function getCommand(item: Formatter.Info) { let cmd = commands[item.name] if (cmd === false || cmd === undefined) { - cmd = await item.enabled() + cmd = await item.enabled(ctx) commands[item.name] = cmd } return cmd @@ -153,7 +153,7 @@ export const layer = Layer.effect( ...info, name, extensions: info.extensions ?? [], - enabled: builtIn && !info.command ? builtIn.enabled : async () => info.command ?? false, + enabled: builtIn && !info.command ? builtIn.enabled : async (_context) => info.command ?? false, } } } From e6fd57165e7dad5796de278b4127614e3e08ba4f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 21:47:59 -0400 Subject: [PATCH 163/335] refactor: remove ambient instance reads from lsp (#23023) --- packages/opencode/src/lsp/client.ts | 39 ++++++------ packages/opencode/src/lsp/lsp.ts | 22 ++++--- packages/opencode/src/lsp/server.ts | 78 +++++++++++------------ packages/opencode/test/lsp/client.test.ts | 3 + 4 files changed, 75 insertions(+), 67 deletions(-) diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index 59a64ca1ed..b20e8ae7f0 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -11,7 +11,6 @@ import z from "zod" import type * as LSPServer from "./server" import { NamedError } from "@opencode-ai/shared/util/error" import { withTimeout } from "../util/timeout" -import { Instance } from "../project/instance" import { Filesystem } from "../util" const DIAGNOSTICS_DEBOUNCE_MS = 150 @@ -39,7 +38,7 @@ export const Event = { ), } -export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) { +export async function create(input: { serverID: string; server: LSPServer.Handle; root: string; directory: string }) { const l = log.clone().tag("serverID", input.serverID) l.info("starting client") @@ -145,33 +144,33 @@ export async function create(input: { serverID: string; server: LSPServer.Handle return connection }, notify: { - async open(input: { path: string }) { - input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path) - const text = await Filesystem.readText(input.path) - const extension = path.extname(input.path) + async open(request: { path: string }) { + request.path = path.isAbsolute(request.path) ? request.path : path.resolve(input.directory, request.path) + const text = await Filesystem.readText(request.path) + const extension = path.extname(request.path) const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext" - const version = files[input.path] + const version = files[request.path] if (version !== undefined) { - log.info("workspace/didChangeWatchedFiles", input) + log.info("workspace/didChangeWatchedFiles", request) await connection.sendNotification("workspace/didChangeWatchedFiles", { changes: [ { - uri: pathToFileURL(input.path).href, + uri: pathToFileURL(request.path).href, type: 2, // Changed }, ], }) const next = version + 1 - files[input.path] = next + files[request.path] = next log.info("textDocument/didChange", { - path: input.path, + path: request.path, version: next, }) await connection.sendNotification("textDocument/didChange", { textDocument: { - uri: pathToFileURL(input.path).href, + uri: pathToFileURL(request.path).href, version: next, }, contentChanges: [{ text }], @@ -179,36 +178,36 @@ export async function create(input: { serverID: string; server: LSPServer.Handle return } - log.info("workspace/didChangeWatchedFiles", input) + log.info("workspace/didChangeWatchedFiles", request) await connection.sendNotification("workspace/didChangeWatchedFiles", { changes: [ { - uri: pathToFileURL(input.path).href, + uri: pathToFileURL(request.path).href, type: 1, // Created }, ], }) - log.info("textDocument/didOpen", input) - diagnostics.delete(input.path) + log.info("textDocument/didOpen", request) + diagnostics.delete(request.path) await connection.sendNotification("textDocument/didOpen", { textDocument: { - uri: pathToFileURL(input.path).href, + uri: pathToFileURL(request.path).href, languageId, version: 0, text, }, }) - files[input.path] = 0 + files[request.path] = 0 return }, }, get diagnostics() { return diagnostics }, - async waitForDiagnostics(input: { path: string }) { + async waitForDiagnostics(request: { path: string }) { const normalizedPath = Filesystem.normalizePath( - path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path), + path.isAbsolute(request.path) ? request.path : path.resolve(input.directory, request.path), ) log.info("waiting for diagnostics", { path: normalizedPath }) let unsub: () => void diff --git a/packages/opencode/src/lsp/lsp.ts b/packages/opencode/src/lsp/lsp.ts index 43c8309870..221385d265 100644 --- a/packages/opencode/src/lsp/lsp.ts +++ b/packages/opencode/src/lsp/lsp.ts @@ -7,12 +7,12 @@ import { pathToFileURL, fileURLToPath } from "url" import * as LSPServer from "./server" import z from "zod" import { Config } from "../config" -import { Instance } from "../project/instance" import { Flag } from "@/flag/flag" import { Process } from "../util" import { spawn as lspspawn } from "./launch" import { Effect, Layer, Context } from "effect" import { InstanceState } from "@/effect" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" const log = Log.create({ service: "lsp" }) @@ -162,7 +162,7 @@ export const layer = Layer.effect( const config = yield* Config.Service const state = yield* InstanceState.make( - Effect.fn("LSP.state")(function* () { + Effect.fn("LSP.state")(function* (ctx) { const cfg = yield* config.get() const servers: Record = {} @@ -187,7 +187,7 @@ export const layer = Layer.effect( servers[name] = { ...existing, id: name, - root: existing?.root ?? (async () => Instance.directory), + root: existing?.root ?? (async (_file, ctx) => ctx.directory), extensions: item.extensions ?? existing?.extensions ?? [], spawn: async (root) => ({ process: lspspawn(item.command[0], item.command.slice(1), { @@ -225,7 +225,10 @@ export const layer = Layer.effect( ) const getClients = Effect.fnUntraced(function* (file: string) { - if (!Instance.containsPath(file)) return [] as LSPClient.Info[] + const ctx = yield* InstanceState.context + if (!AppFileSystem.contains(ctx.directory, file) && (ctx.worktree === "/" || !AppFileSystem.contains(ctx.worktree, file))) { + return [] as LSPClient.Info[] + } const s = yield* InstanceState.get(state) return yield* Effect.promise(async () => { const extension = path.parse(file).ext || file @@ -233,7 +236,7 @@ export const layer = Layer.effect( async function schedule(server: LSPServer.Info, root: string, key: string) { const handle = await server - .spawn(root) + .spawn(root, ctx) .then((value) => { if (!value) s.broken.add(key) return value @@ -251,6 +254,7 @@ export const layer = Layer.effect( serverID: server.id, server: handle, root, + directory: ctx.directory, }).catch(async (err) => { s.broken.add(key) await Process.stop(handle.process) @@ -273,7 +277,7 @@ export const layer = Layer.effect( for (const server of Object.values(s.servers)) { if (server.extensions.length && !server.extensions.includes(extension)) continue - const root = await server.root(file) + const root = await server.root(file, ctx) if (!root) continue if (s.broken.has(root + server.id)) continue @@ -326,13 +330,14 @@ export const layer = Layer.effect( }) const status = Effect.fn("LSP.status")(function* () { + const ctx = yield* InstanceState.context const s = yield* InstanceState.get(state) const result: Status[] = [] for (const client of s.clients) { result.push({ id: client.serverID, name: s.servers[client.serverID].id, - root: path.relative(Instance.directory, client.root), + root: path.relative(ctx.directory, client.root), status: "connected", }) } @@ -340,12 +345,13 @@ export const layer = Layer.effect( }) const hasClients = Effect.fn("LSP.hasClients")(function* (file: string) { + const ctx = yield* InstanceState.context const s = yield* InstanceState.get(state) return yield* Effect.promise(async () => { const extension = path.parse(file).ext || file for (const server of Object.values(s.servers)) { if (server.extensions.length && !server.extensions.includes(extension)) continue - const root = await server.root(file) + const root = await server.root(file, ctx) if (!root) continue if (s.broken.has(root + server.id)) continue return true diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 760e8eaba0..e0221c9dd5 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -6,7 +6,7 @@ import { Log } from "../util" import { text } from "node:stream/consumers" import fs from "fs/promises" import { Filesystem } from "../util" -import { Instance } from "../project/instance" +import type { InstanceContext } from "../project/instance" import { Flag } from "../flag/flag" import { Archive } from "../util" import { Process } from "../util" @@ -29,15 +29,15 @@ export interface Handle { initialization?: Record } -type RootFunction = (file: string) => Promise +type RootFunction = (file: string, ctx: InstanceContext) => Promise const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): RootFunction => { - return async (file) => { + return async (file, ctx) => { if (excludePatterns) { const excludedFiles = Filesystem.up({ targets: excludePatterns, start: path.dirname(file), - stop: Instance.directory, + stop: ctx.directory, }) const excluded = await excludedFiles.next() await excludedFiles.return() @@ -46,11 +46,11 @@ const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): Roo const files = Filesystem.up({ targets: includePatterns, start: path.dirname(file), - stop: Instance.directory, + stop: ctx.directory, }) const first = await files.next() await files.return() - if (!first.value) return Instance.directory + if (!first.value) return ctx.directory return path.dirname(first.value) } } @@ -60,16 +60,16 @@ export interface Info { extensions: string[] global?: boolean root: RootFunction - spawn(root: string): Promise + spawn(root: string, ctx: InstanceContext): Promise } export const Deno: Info = { id: "deno", - root: async (file) => { + root: async (file, ctx) => { const files = Filesystem.up({ targets: ["deno.json", "deno.jsonc"], start: path.dirname(file), - stop: Instance.directory, + stop: ctx.directory, }) const first = await files.next() await files.return() @@ -98,8 +98,8 @@ export const Typescript: Info = { ["deno.json", "deno.jsonc"], ), extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"], - async spawn(root) { - const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory) + async spawn(root, ctx) { + const tsserver = Module.resolve("typescript/lib/tsserver.js", ctx.directory) log.info("typescript server", { tsserver }) if (!tsserver) return const bin = await Npm.which("typescript-language-server") @@ -154,8 +154,8 @@ export const ESLint: Info = { id: "eslint", root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"], - async spawn(root) { - const eslint = Module.resolve("eslint", Instance.directory) + async spawn(root, ctx) { + const eslint = Module.resolve("eslint", ctx.directory) if (!eslint) return log.info("spawning eslint server") const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js") @@ -219,7 +219,7 @@ export const Oxlint: Info = { "package.json", ]), extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue", ".astro", ".svelte"], - async spawn(root) { + async spawn(root, ctx) { const ext = process.platform === "win32" ? ".cmd" : "" const serverTarget = path.join("node_modules", ".bin", "oxc_language_server" + ext) @@ -232,7 +232,7 @@ export const Oxlint: Info = { const candidates = Filesystem.up({ targets: [target], start: root, - stop: Instance.worktree, + stop: ctx.worktree, }) const first = await candidates.next() await candidates.return() @@ -344,10 +344,10 @@ export const Biome: Info = { export const Gopls: Info = { id: "gopls", - root: async (file) => { - const work = await NearestRoot(["go.work"])(file) + root: async (file, ctx) => { + const work = await NearestRoot(["go.work"])(file, ctx) if (work) return work - return NearestRoot(["go.mod", "go.sum"])(file) + return NearestRoot(["go.mod", "go.sum"])(file, ctx) }, extensions: [".go"], async spawn(root) { @@ -810,8 +810,8 @@ export const SourceKit: Info = { export const RustAnalyzer: Info = { id: "rust", - root: async (root) => { - const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(root) + root: async (file, ctx) => { + const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(file, ctx) if (crateRoot === undefined) { return undefined } @@ -834,7 +834,7 @@ export const RustAnalyzer: Info = { currentDir = parentDir // Stop if we've gone above the app root - if (!currentDir.startsWith(Instance.worktree)) break + if (!currentDir.startsWith(ctx.worktree)) break } return crateRoot @@ -1031,8 +1031,8 @@ export const Astro: Info = { id: "astro", extensions: [".astro"], root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), - async spawn(root) { - const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory) + async spawn(root, ctx) { + const tsserver = Module.resolve("typescript/lib/tsserver.js", ctx.directory) if (!tsserver) { log.info("typescript not found, required for Astro language server") return @@ -1067,7 +1067,7 @@ export const Astro: Info = { export const JDTLS: Info = { id: "jdtls", - root: async (file) => { + root: async (file, ctx) => { // Without exclusions, NearestRoot defaults to instance directory so we can't // distinguish between a) no project found and b) project found at instance dir. // So we can't choose the root from (potential) monorepo markers first. @@ -1080,9 +1080,9 @@ export const JDTLS: Info = { NearestRoot( ["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"], exclusionsForMonorepos, - )(file), - NearestRoot(gradleMarkers, settingsMarkers)(file), - NearestRoot(settingsMarkers)(file), + )(file, ctx), + NearestRoot(gradleMarkers, settingsMarkers)(file, ctx), + NearestRoot(settingsMarkers)(file, ctx), ]) // If projectRoot is undefined we know we are in a monorepo or no project at all. @@ -1189,18 +1189,18 @@ export const JDTLS: Info = { export const KotlinLS: Info = { id: "kotlin-ls", extensions: [".kt", ".kts"], - root: async (file) => { + root: async (file, ctx) => { // 1) Nearest Gradle root (multi-project or included build) - const settingsRoot = await NearestRoot(["settings.gradle.kts", "settings.gradle"])(file) + const settingsRoot = await NearestRoot(["settings.gradle.kts", "settings.gradle"])(file, ctx) if (settingsRoot) return settingsRoot // 2) Gradle wrapper (strong root signal) - const wrapperRoot = await NearestRoot(["gradlew", "gradlew.bat"])(file) + const wrapperRoot = await NearestRoot(["gradlew", "gradlew.bat"])(file, ctx) if (wrapperRoot) return wrapperRoot // 3) Single-project or module-level build - const buildRoot = await NearestRoot(["build.gradle.kts", "build.gradle"])(file) + const buildRoot = await NearestRoot(["build.gradle.kts", "build.gradle"])(file, ctx) if (buildRoot) return buildRoot // 4) Maven fallback - return NearestRoot(["pom.xml"])(file) + return NearestRoot(["pom.xml"])(file, ctx) }, async spawn(root) { const distPath = path.join(Global.Path.bin, "kotlin-ls") @@ -1539,7 +1539,7 @@ export const Ocaml: Info = { export const BashLS: Info = { id: "bash", extensions: [".sh", ".bash", ".zsh", ".ksh"], - root: async () => Instance.directory, + root: async (_file, ctx) => ctx.directory, async spawn(root) { let binary = which("bash-language-server") const args: string[] = [] @@ -1734,7 +1734,7 @@ export const TexLab: Info = { export const DockerfileLS: Info = { id: "dockerfile", extensions: [".dockerfile", "Dockerfile"], - root: async () => Instance.directory, + root: async (_file, ctx) => ctx.directory, async spawn(root) { let binary = which("docker-langserver") const args: string[] = [] @@ -1799,16 +1799,16 @@ export const Clojure: Info = { export const Nixd: Info = { id: "nixd", extensions: [".nix"], - root: async (file) => { + root: async (file, ctx) => { // First, look for flake.nix - the most reliable Nix project root indicator - const flakeRoot = await NearestRoot(["flake.nix"])(file) - if (flakeRoot && flakeRoot !== Instance.directory) return flakeRoot + const flakeRoot = await NearestRoot(["flake.nix"])(file, ctx) + if (flakeRoot && flakeRoot !== ctx.directory) return flakeRoot // If no flake.nix, fall back to git repository root - if (Instance.worktree && Instance.worktree !== Instance.directory) return Instance.worktree + if (ctx.worktree && ctx.worktree !== ctx.directory) return ctx.worktree // Finally, use the instance directory as fallback - return Instance.directory + return ctx.directory }, async spawn(root) { const nixd = which("nixd") diff --git a/packages/opencode/test/lsp/client.test.ts b/packages/opencode/test/lsp/client.test.ts index f124fddf95..d6eaa317f9 100644 --- a/packages/opencode/test/lsp/client.test.ts +++ b/packages/opencode/test/lsp/client.test.ts @@ -31,6 +31,7 @@ describe("LSPClient interop", () => { serverID: "fake", server: handle as unknown as LSPServer.Handle, root: process.cwd(), + directory: process.cwd(), }), }) @@ -55,6 +56,7 @@ describe("LSPClient interop", () => { serverID: "fake", server: handle as unknown as LSPServer.Handle, root: process.cwd(), + directory: process.cwd(), }), }) @@ -79,6 +81,7 @@ describe("LSPClient interop", () => { serverID: "fake", server: handle as unknown as LSPServer.Handle, root: process.cwd(), + directory: process.cwd(), }), }) From 866188a643271ddfca3c3ae007d6cddad8d0d11c Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 18 Apr 2026 01:48:50 +0000 Subject: [PATCH 164/335] chore: generate --- packages/opencode/src/lsp/lsp.ts | 5 ++++- packages/opencode/src/lsp/server.ts | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/lsp/lsp.ts b/packages/opencode/src/lsp/lsp.ts index 221385d265..aa519f9f7e 100644 --- a/packages/opencode/src/lsp/lsp.ts +++ b/packages/opencode/src/lsp/lsp.ts @@ -226,7 +226,10 @@ export const layer = Layer.effect( const getClients = Effect.fnUntraced(function* (file: string) { const ctx = yield* InstanceState.context - if (!AppFileSystem.contains(ctx.directory, file) && (ctx.worktree === "/" || !AppFileSystem.contains(ctx.worktree, file))) { + if ( + !AppFileSystem.contains(ctx.directory, file) && + (ctx.worktree === "/" || !AppFileSystem.contains(ctx.worktree, file)) + ) { return [] as LSPClient.Info[] } const s = yield* InstanceState.get(state) diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index e0221c9dd5..9182368063 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -1077,10 +1077,10 @@ export const JDTLS: Info = { const exclusionsForMonorepos = gradleMarkers.concat(settingsMarkers) const [projectRoot, wrapperRoot, settingsRoot] = await Promise.all([ - NearestRoot( - ["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"], - exclusionsForMonorepos, - )(file, ctx), + NearestRoot(["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"], exclusionsForMonorepos)( + file, + ctx, + ), NearestRoot(gradleMarkers, settingsMarkers)(file, ctx), NearestRoot(settingsMarkers)(file, ctx), ]) From 81b7b58a5e396fb7d932ac77c7503d6271fa30ea Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:57:48 -0500 Subject: [PATCH 165/335] fix: gh copilot issue w/ haiku (eager_input_streaming not supported) (#23223) --- packages/opencode/src/plugin/github-copilot/copilot.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/opencode/src/plugin/github-copilot/copilot.ts b/packages/opencode/src/plugin/github-copilot/copilot.ts index c018f72bd5..9b6f54459d 100644 --- a/packages/opencode/src/plugin/github-copilot/copilot.ts +++ b/packages/opencode/src/plugin/github-copilot/copilot.ts @@ -334,6 +334,13 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { if (incoming.model.api.id.includes("gpt")) { output.maxOutputTokens = undefined } + + // GitHub Copilot's /v1/messages shim rejects the GA `eager_input_streaming` + // field on tool definitions ("Extra inputs are not permitted"). Opt out of + // the @ai-sdk/anthropic default so it stops injecting the field. + if (incoming.model.api.npm === "@ai-sdk/anthropic") { + output.options.toolStreaming = false + } }, "chat.headers": async (incoming, output) => { if (!incoming.model.providerID.includes("github-copilot")) return From 1fae784b81a01fed3dcd6209e26427410158af30 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 22:02:37 -0400 Subject: [PATCH 166/335] feat(effect-zod): add ZodPreprocess annotation for pre-parse transforms (#23222) --- packages/opencode/src/util/effect-zod.ts | 41 +++++- .../opencode/test/util/effect-zod.test.ts | 118 +++++++++++++++++- 2 files changed, 157 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/util/effect-zod.ts b/packages/opencode/src/util/effect-zod.ts index cdcd99c976..bf1caa035b 100644 --- a/packages/opencode/src/util/effect-zod.ts +++ b/packages/opencode/src/util/effect-zod.ts @@ -8,6 +8,43 @@ import z from "zod" */ export const ZodOverride: unique symbol = Symbol.for("effect-zod/override") +/** + * Annotation key for a pre-parse transform that runs on the raw input before + * the derived Zod schema validates it. The walker emits + * `z.preprocess(fn, inner)` when this annotation is present. + * + * Models zod's `z.preprocess(fn, schema)` pattern — useful when the schema + * needs to inspect the user's raw input (e.g. to capture insertion order) + * before `Schema.Struct` canonicalises the object. + * + * TODO: This exists to paper over a missing Effect Schema feature. The + * parser canonicalises open struct output (known fields first in + * declaration order, then catchall fields) before any user-defined + * transform sees the value, and there is no pre-parse hook — so the + * user's original property insertion order is gone by the time + * `Schema.decodeTo` or `middlewareDecoding` runs. + * + * That canonicalisation is a reasonable default, but `config/permission.ts` + * encodes rule precedence in the user's JSON key order (`evaluate.ts` + * uses `findLast`, so later entries win), which the canonicalisation + * silently destroys. + * + * The cleanest upstream fix would be either: + * + * 1. A `preserveInputOrder` option on `Schema.Struct` / + * `Schema.StructWithRest` that keeps the input's insertion order in + * the parsed object (opt-in; canonical order stays default). + * 2. A generic pre-parse hook (`Schema.preprocess(schema, fn)` or a + * transformation whose decode receives the raw `unknown`). + * + * Either of those would let us delete `ZodPreprocess` and the + * `__originalKeys` hack. Alternatively, the permission model could move + * to specificity-based precedence (exact keys beat wildcards) or an + * explicit ordered array of rules, which removes the ordering + * dependency at the data-model level. + */ +export const ZodPreprocess: unique symbol = Symbol.for("effect-zod/preprocess") + // AST nodes are immutable and frequently shared across schemas (e.g. a single // Schema.Class embedded in multiple parents). Memoizing by node identity // avoids rebuilding equivalent Zod subtrees and keeps derived children stable @@ -47,7 +84,9 @@ function walkUncached(ast: SchemaAST.AST): z.ZodTypeAny { const hasEncoding = ast.encoding?.length && ast._tag !== "Declaration" const hasTransform = hasEncoding && !(SchemaAST.isOptional(ast) && extractDefault(ast) !== undefined) const base = hasTransform ? encoded(ast) : body(ast) - const out = ast.checks?.length ? applyChecks(base, ast.checks, ast) : base + const checked = ast.checks?.length ? applyChecks(base, ast.checks, ast) : base + const preprocess = (ast.annotations as { [ZodPreprocess]?: (val: unknown) => unknown } | undefined)?.[ZodPreprocess] + const out = preprocess ? z.preprocess(preprocess, checked) : checked const desc = SchemaAST.resolveDescription(ast) const ref = SchemaAST.resolveIdentifier(ast) const described = desc ? out.describe(desc) : out diff --git a/packages/opencode/test/util/effect-zod.test.ts b/packages/opencode/test/util/effect-zod.test.ts index 70cd8f0e64..9bf3de3f84 100644 --- a/packages/opencode/test/util/effect-zod.test.ts +++ b/packages/opencode/test/util/effect-zod.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test" import { Effect, Schema, SchemaGetter } from "effect" import z from "zod" -import { zod, ZodOverride } from "../../src/util/effect-zod" +import { zod, ZodOverride, ZodPreprocess } from "../../src/util/effect-zod" function json(schema: z.ZodTypeAny) { const { $schema: _, ...rest } = z.toJSONSchema(schema) @@ -751,4 +751,120 @@ describe("util.effect-zod", () => { expect(schema.parse({ foo: "hi" })).toEqual({ foo: "hi" }) }) }) + + describe("ZodPreprocess annotation", () => { + test("preprocess runs on raw input before the inner schema parses", () => { + // Models the permission.ts __originalKeys pattern: capture the original + // insertion order of a user-provided object BEFORE Schema parsing + // canonicalises the keys. + const preprocess = (val: unknown) => { + if (typeof val === "object" && val !== null && !Array.isArray(val)) { + return { __keys: Object.keys(val), ...(val as Record) } + } + return val + } + const Inner = Schema.Struct({ + __keys: Schema.optional(Schema.mutable(Schema.Array(Schema.String))), + a: Schema.optional(Schema.String), + b: Schema.optional(Schema.String), + }).annotate({ [ZodPreprocess]: preprocess }) + + const schema = zod(Inner) + const parsed = schema.parse({ b: "1", a: "2" }) as { + __keys?: string[] + a?: string + b?: string + } + expect(parsed.__keys).toEqual(["b", "a"]) + expect(parsed.a).toBe("2") + expect(parsed.b).toBe("1") + }) + + test("preprocess does not transform already-shaped input", () => { + // When the user passes an object that already has __keys, preprocess + // returns it unchanged because spreading preserves any existing key. + const preprocess = (val: unknown) => { + if (typeof val === "object" && val !== null && !("__keys" in val)) { + return { __keys: Object.keys(val), ...(val as Record) } + } + return val + } + const Inner = Schema.Struct({ + __keys: Schema.optional(Schema.mutable(Schema.Array(Schema.String))), + a: Schema.optional(Schema.String), + }).annotate({ [ZodPreprocess]: preprocess }) + + const schema = zod(Inner) + const parsed = schema.parse({ __keys: ["existing"], a: "hi" }) as { + __keys?: string[] + a?: string + } + expect(parsed.__keys).toEqual(["existing"]) + }) + + test("preprocess composes with a union (either object or string)", () => { + // Mirrors permission.ts exactly: input can be either an object (with + // preprocess injecting metadata) or a plain string action. + const Action = Schema.Literals(["ask", "allow", "deny"]) + const Obj = Schema.Struct({ + __keys: Schema.optional(Schema.mutable(Schema.Array(Schema.String))), + read: Schema.optional(Action), + write: Schema.optional(Action), + }) + const preprocess = (val: unknown) => { + if (typeof val === "object" && val !== null && !Array.isArray(val)) { + return { __keys: Object.keys(val), ...(val as Record) } + } + return val + } + const Inner = Schema.Union([Obj, Action]).annotate({ [ZodPreprocess]: preprocess }) + const schema = zod(Inner) + + // String branch — passes through preprocess unchanged + expect(schema.parse("allow")).toBe("allow") + + // Object branch — __keys injected, preserves order + const parsed = schema.parse({ write: "allow", read: "deny" }) as { + __keys?: string[] + read?: string + write?: string + } + expect(parsed.__keys).toEqual(["write", "read"]) + expect(parsed.write).toBe("allow") + expect(parsed.read).toBe("deny") + }) + + test("JSON Schema output comes from the inner schema — preprocess is runtime-only", () => { + const Inner = Schema.Struct({ + a: Schema.optional(Schema.String), + b: Schema.optional(Schema.Number), + }).annotate({ [ZodPreprocess]: (v: unknown) => v }) + const shape = json(zod(Inner)) as any + expect(shape.type).toBe("object") + expect(shape.properties.a.type).toBe("string") + expect(shape.properties.b.type).toBe("number") + }) + + test("identifier + description propagate through the preprocess wrapper", () => { + const Inner = Schema.Struct({ + x: Schema.optional(Schema.String), + }) + .annotate({ + identifier: "WithPreproc", + description: "A schema with preprocess", + [ZodPreprocess]: (v: unknown) => v, + }) + const schema = zod(Inner) + expect(schema.meta()?.ref).toBe("WithPreproc") + expect(schema.meta()?.description).toBe("A schema with preprocess") + }) + + test("preprocess inside a struct field applies only to that field", () => { + const Inner = Schema.String.annotate({ + [ZodPreprocess]: (v: unknown) => (typeof v === "number" ? String(v) : v), + }) + const schema = zod(Schema.Struct({ name: Inner, raw: Schema.Number })) + expect(schema.parse({ name: 42, raw: 7 })).toEqual({ name: "42", raw: 7 }) + }) + }) }) From daaa1c7e26a06d2239133c857d7f8863b60fb97a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 18 Apr 2026 02:03:30 +0000 Subject: [PATCH 167/335] chore: generate --- packages/opencode/test/util/effect-zod.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/opencode/test/util/effect-zod.test.ts b/packages/opencode/test/util/effect-zod.test.ts index 9bf3de3f84..003945b434 100644 --- a/packages/opencode/test/util/effect-zod.test.ts +++ b/packages/opencode/test/util/effect-zod.test.ts @@ -848,12 +848,11 @@ describe("util.effect-zod", () => { test("identifier + description propagate through the preprocess wrapper", () => { const Inner = Schema.Struct({ x: Schema.optional(Schema.String), + }).annotate({ + identifier: "WithPreproc", + description: "A schema with preprocess", + [ZodPreprocess]: (v: unknown) => v, }) - .annotate({ - identifier: "WithPreproc", - description: "A schema with preprocess", - [ZodPreprocess]: (v: unknown) => v, - }) const schema = zod(Inner) expect(schema.meta()?.ref).toBe("WithPreproc") expect(schema.meta()?.description).toBe("A schema with preprocess") From 5fa1673341f2ebde860cb06799626c6dfbcde2c3 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 22:08:57 -0400 Subject: [PATCH 168/335] refactor: use InstanceState context in File service (#23015) --- packages/opencode/src/file/index.ts | 48 +++++++++++++---------- packages/opencode/src/project/instance.ts | 2 +- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index 2f30b5400d..af4fbf76c8 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -356,8 +356,9 @@ export const layer = Layer.effect( ) const scan = Effect.fn("File.scan")(function* () { - if (Instance.directory === path.parse(Instance.directory).root) return - const isGlobalHome = Instance.directory === Global.Path.home && Instance.project.id === "global" + const ctx = yield* InstanceState.context + if (ctx.directory === path.parse(ctx.directory).root) return + const isGlobalHome = ctx.directory === Global.Path.home && ctx.project.id === "global" const next: Entry = { files: [], dirs: [] } if (isGlobalHome) { @@ -366,14 +367,14 @@ export const layer = Layer.effect( const ignoreNested = new Set(["node_modules", "dist", "build", "target", "vendor"]) const shouldIgnoreName = (name: string) => name.startsWith(".") || protectedNames.has(name) const shouldIgnoreNested = (name: string) => name.startsWith(".") || ignoreNested.has(name) - const top = yield* appFs.readDirectoryEntries(Instance.directory).pipe(Effect.orElseSucceed(() => [])) + const top = yield* appFs.readDirectoryEntries(ctx.directory).pipe(Effect.orElseSucceed(() => [])) for (const entry of top) { if (entry.type !== "directory") continue if (shouldIgnoreName(entry.name)) continue dirs.add(entry.name + "/") - const base = path.join(Instance.directory, entry.name) + const base = path.join(ctx.directory, entry.name) const children = yield* appFs.readDirectoryEntries(base).pipe(Effect.orElseSucceed(() => [])) for (const child of children) { if (child.type !== "directory") continue @@ -384,7 +385,7 @@ export const layer = Layer.effect( next.dirs = Array.from(dirs).toSorted() } else { - const files = yield* rg.files({ cwd: Instance.directory }).pipe( + const files = yield* rg.files({ cwd: ctx.directory }).pipe( Stream.runCollect, Effect.map((chunk) => [...chunk]), ) @@ -416,7 +417,7 @@ export const layer = Layer.effect( }) const gitText = Effect.fnUntraced(function* (args: string[]) { - return (yield* git.run(args, { cwd: Instance.directory })).text() + return (yield* git.run(args, { cwd: (yield* InstanceState.context).directory })).text() }) const init = Effect.fn("File.init")(function* () { @@ -424,7 +425,8 @@ export const layer = Layer.effect( }) const status = Effect.fn("File.status")(function* () { - if (Instance.project.vcs !== "git") return [] + const ctx = yield* InstanceState.context + if (ctx.project.vcs !== "git") return [] const diffOutput = yield* gitText([ "-c", @@ -463,7 +465,7 @@ export const layer = Layer.effect( if (untrackedOutput.trim()) { for (const file of untrackedOutput.trim().split("\n")) { const content = yield* appFs - .readFileString(path.join(Instance.directory, file)) + .readFileString(path.join(ctx.directory, file)) .pipe(Effect.catch(() => Effect.succeed(undefined))) if (content === undefined) continue changed.push({ @@ -498,19 +500,22 @@ export const layer = Layer.effect( } return changed.map((item) => { - const full = path.isAbsolute(item.path) ? item.path : path.join(Instance.directory, item.path) + const full = path.isAbsolute(item.path) ? item.path : path.join(ctx.directory, item.path) return { ...item, - path: path.relative(Instance.directory, full), + path: path.relative(ctx.directory, full), } }) }) const read: Interface["read"] = Effect.fn("File.read")(function* (file: string) { using _ = log.time("read", { file }) - const full = path.join(Instance.directory, file) + const ctx = yield* InstanceState.context + const full = path.join(ctx.directory, file) - if (!Instance.containsPath(full)) throw new Error("Access denied: path escapes project directory") + if (!Instance.containsPath(full, ctx)) { + throw new Error("Access denied: path escapes project directory") + } if (isImageByExtension(file)) { const exists = yield* appFs.existsSafe(full) @@ -553,13 +558,13 @@ export const layer = Layer.effect( Effect.catch(() => Effect.succeed("")), ) - if (Instance.project.vcs === "git") { + if (ctx.project.vcs === "git") { let diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--", file]) if (!diff.trim()) { diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file]) } if (diff.trim()) { - const original = yield* git.show(Instance.directory, "HEAD", file) + const original = yield* git.show(ctx.directory, "HEAD", file) const patch = structuredPatch(file, file, original, content, "old", "new", { context: Infinity, ignoreWhitespace: true, @@ -573,21 +578,24 @@ export const layer = Layer.effect( }) const list = Effect.fn("File.list")(function* (dir?: string) { + const ctx = yield* InstanceState.context const exclude = [".git", ".DS_Store"] let ignored = (_: string) => false - if (Instance.project.vcs === "git") { + if (ctx.project.vcs === "git") { const ig = ignore() - const gitignore = path.join(Instance.project.worktree, ".gitignore") + const gitignore = path.join(ctx.worktree, ".gitignore") const gitignoreText = yield* appFs.readFileString(gitignore).pipe(Effect.catch(() => Effect.succeed(""))) if (gitignoreText) ig.add(gitignoreText) - const ignoreFile = path.join(Instance.project.worktree, ".ignore") + const ignoreFile = path.join(ctx.worktree, ".ignore") const ignoreText = yield* appFs.readFileString(ignoreFile).pipe(Effect.catch(() => Effect.succeed(""))) if (ignoreText) ig.add(ignoreText) ignored = ig.ignores.bind(ig) } - const resolved = dir ? path.join(Instance.directory, dir) : Instance.directory - if (!Instance.containsPath(resolved)) throw new Error("Access denied: path escapes project directory") + const resolved = dir ? path.join(ctx.directory, dir) : ctx.directory + if (!Instance.containsPath(resolved, ctx)) { + throw new Error("Access denied: path escapes project directory") + } const entries = yield* appFs.readDirectoryEntries(resolved).pipe(Effect.orElseSucceed(() => [])) @@ -595,7 +603,7 @@ export const layer = Layer.effect( for (const entry of entries) { if (exclude.includes(entry.name)) continue const absolute = path.join(resolved, entry.name) - const file = path.relative(Instance.directory, absolute) + const file = path.relative(ctx.directory, absolute) const type = entry.type === "directory" ? "directory" : "file" nodes.push({ name: entry.name, diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index 056eede01b..1c51096204 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -96,7 +96,7 @@ export const Instance = { if (AppFileSystem.contains(instance.directory, filepath)) return true // Non-git projects set worktree to "/" which would match ANY absolute path. // Skip worktree check in this case to preserve external_directory permissions. - if (Instance.worktree === "/") return false + if (instance.worktree === "/") return false return AppFileSystem.contains(instance.worktree, filepath) }, /** From 1dd257b76a98a159f763ec009bf061e6bd4ae718 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 22:16:15 -0400 Subject: [PATCH 169/335] refactor: use instance state in small services (#23022) --- packages/opencode/src/mcp/index.ts | 3 +-- packages/opencode/src/project/vcs.ts | 15 +++++---------- packages/opencode/src/provider/provider.ts | 7 ++----- packages/opencode/src/session/instruction.ts | 9 +++++---- 4 files changed, 13 insertions(+), 21 deletions(-) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index ba53e7c0b5..09fcfc756a 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -14,7 +14,6 @@ import { ConfigMCP } from "../config/mcp" import { Log } from "../util" import { NamedError } from "@opencode-ai/shared/util/error" import z from "zod/v4" -import { Instance } from "../project/instance" import { Installation } from "../installation" import { InstallationVersion } from "../installation/version" import { withTimeout } from "@/util/timeout" @@ -391,7 +390,7 @@ export const layer = Layer.effect( mcp: ConfigMCP.Info & { type: "local" }, ) { const [cmd, ...args] = mcp.command - const cwd = Instance.directory + const cwd = yield* InstanceState.directory const transport = new StdioClientTransport({ stderr: "pipe", command: cmd, diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts index b1375a7b78..ba028f7e8e 100644 --- a/packages/opencode/src/project/vcs.ts +++ b/packages/opencode/src/project/vcs.ts @@ -8,7 +8,6 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { FileWatcher } from "@/file/watcher" import { Git } from "@/git" import { Log } from "@/util" -import { Instance } from "./instance" import z from "zod" const log = Log.create({ service: "vcs" }) @@ -205,21 +204,17 @@ export const layer: Layer.Layer { const token = apiKey ?? (yield* dep.get("GITLAB_TOKEN")) const providerConfig = (yield* dep.config()).provider?.["gitlab"] + const directory = yield* InstanceState.directory const aiGatewayHeaders = { "User-Agent": `opencode/${InstallationVersion} gitlab-ai-provider/${GITLAB_PROVIDER_VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`, @@ -591,10 +591,7 @@ function custom(dep: CustomDep): Record { auth?.type === "api" ? { "PRIVATE-TOKEN": token } : { Authorization: `Bearer ${token}` } log.info("gitlab model discovery starting", { instanceUrl }) - const result = await discoverWorkflowModels( - { instanceUrl, getHeaders }, - { workingDirectory: Instance.directory }, - ) + const result = await discoverWorkflowModels({ instanceUrl, getHeaders }, { workingDirectory: directory }) if (!result.models.length) { log.info("gitlab model discovery skipped: no models found", { diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index 768f352d93..122644c1fd 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -8,7 +8,6 @@ import { Flag } from "@/flag/flag" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { withTransientReadRetry } from "@/util/effect-http-client" import { Global } from "../global" -import { Instance } from "../project/instance" import { Log } from "../util" import type { MessageV2 } from "./message-v2" import type { MessageID } from "./schema" @@ -82,9 +81,10 @@ export const layer: Layer.Layer Effect.succeed([] as string[]))) } if (!Flag.OPENCODE_CONFIG_DIR) { @@ -119,12 +119,13 @@ export const layer: Layer.Layer() // The first project-level match wins so we don't stack AGENTS.md/CLAUDE.md from every ancestor. if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) { for (const file of FILES) { - const matches = yield* fs.findUp(file, Instance.directory, Instance.worktree) + const matches = yield* fs.findUp(file, ctx.directory, ctx.worktree) if (matches.length > 0) { matches.forEach((item) => paths.add(path.resolve(item))) break @@ -191,9 +192,9 @@ export const layer: Layer.Layer Date: Fri, 17 Apr 2026 22:28:45 -0400 Subject: [PATCH 170/335] refactor(config): migrate keybinds.ts to Effect Schema (#23227) --- packages/opencode/src/config/keybinds.ts | 287 ++++++++++------------- 1 file changed, 124 insertions(+), 163 deletions(-) diff --git a/packages/opencode/src/config/keybinds.ts b/packages/opencode/src/config/keybinds.ts index 8a22289d2a..a84fc0b37d 100644 --- a/packages/opencode/src/config/keybinds.ts +++ b/packages/opencode/src/config/keybinds.ts @@ -1,166 +1,127 @@ export * as ConfigKeybinds from "./keybinds" -import z from "zod" +import { Effect, Schema } from "effect" +import type z from "zod" +import { zod } from "@/util/effect-zod" -export const Keybinds = z - .object({ - leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"), - app_exit: z.string().optional().default("ctrl+c,ctrl+d,q").describe("Exit the application"), - editor_open: z.string().optional().default("e").describe("Open external editor"), - theme_list: z.string().optional().default("t").describe("List available themes"), - sidebar_toggle: z.string().optional().default("b").describe("Toggle sidebar"), - scrollbar_toggle: z.string().optional().default("none").describe("Toggle session scrollbar"), - username_toggle: z.string().optional().default("none").describe("Toggle username visibility"), - status_view: z.string().optional().default("s").describe("View status"), - session_export: z.string().optional().default("x").describe("Export session to editor"), - session_new: z.string().optional().default("n").describe("Create a new session"), - session_list: z.string().optional().default("l").describe("List all sessions"), - session_timeline: z.string().optional().default("g").describe("Show session timeline"), - session_fork: z.string().optional().default("none").describe("Fork session from message"), - session_rename: z.string().optional().default("ctrl+r").describe("Rename session"), - session_delete: z.string().optional().default("ctrl+d").describe("Delete session"), - stash_delete: z.string().optional().default("ctrl+d").describe("Delete stash entry"), - model_provider_list: z.string().optional().default("ctrl+a").describe("Open provider list from model dialog"), - model_favorite_toggle: z.string().optional().default("ctrl+f").describe("Toggle model favorite status"), - session_share: z.string().optional().default("none").describe("Share current session"), - session_unshare: z.string().optional().default("none").describe("Unshare current session"), - session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"), - session_compact: z.string().optional().default("c").describe("Compact the session"), - messages_page_up: z.string().optional().default("pageup,ctrl+alt+b").describe("Scroll messages up by one page"), - messages_page_down: z - .string() - .optional() - .default("pagedown,ctrl+alt+f") - .describe("Scroll messages down by one page"), - messages_line_up: z.string().optional().default("ctrl+alt+y").describe("Scroll messages up by one line"), - messages_line_down: z.string().optional().default("ctrl+alt+e").describe("Scroll messages down by one line"), - messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"), - messages_half_page_down: z.string().optional().default("ctrl+alt+d").describe("Scroll messages down by half page"), - messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"), - messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"), - messages_next: z.string().optional().default("none").describe("Navigate to next message"), - messages_previous: z.string().optional().default("none").describe("Navigate to previous message"), - messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"), - messages_copy: z.string().optional().default("y").describe("Copy message"), - messages_undo: z.string().optional().default("u").describe("Undo message"), - messages_redo: z.string().optional().default("r").describe("Redo message"), - messages_toggle_conceal: z - .string() - .optional() - .default("h") - .describe("Toggle code block concealment in messages"), - tool_details: z.string().optional().default("none").describe("Toggle tool details visibility"), - model_list: z.string().optional().default("m").describe("List available models"), - model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"), - model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"), - model_cycle_favorite: z.string().optional().default("none").describe("Next favorite model"), - model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"), - command_list: z.string().optional().default("ctrl+p").describe("List available commands"), - agent_list: z.string().optional().default("a").describe("List agents"), - agent_cycle: z.string().optional().default("tab").describe("Next agent"), - agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"), - variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"), - variant_list: z.string().optional().default("none").describe("List model variants"), - input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"), - input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"), - input_submit: z.string().optional().default("return").describe("Submit input"), - input_newline: z - .string() - .optional() - .default("shift+return,ctrl+return,alt+return,ctrl+j") - .describe("Insert newline in input"), - input_move_left: z.string().optional().default("left,ctrl+b").describe("Move cursor left in input"), - input_move_right: z.string().optional().default("right,ctrl+f").describe("Move cursor right in input"), - input_move_up: z.string().optional().default("up").describe("Move cursor up in input"), - input_move_down: z.string().optional().default("down").describe("Move cursor down in input"), - input_select_left: z.string().optional().default("shift+left").describe("Select left in input"), - input_select_right: z.string().optional().default("shift+right").describe("Select right in input"), - input_select_up: z.string().optional().default("shift+up").describe("Select up in input"), - input_select_down: z.string().optional().default("shift+down").describe("Select down in input"), - input_line_home: z.string().optional().default("ctrl+a").describe("Move to start of line in input"), - input_line_end: z.string().optional().default("ctrl+e").describe("Move to end of line in input"), - input_select_line_home: z.string().optional().default("ctrl+shift+a").describe("Select to start of line in input"), - input_select_line_end: z.string().optional().default("ctrl+shift+e").describe("Select to end of line in input"), - input_visual_line_home: z.string().optional().default("alt+a").describe("Move to start of visual line in input"), - input_visual_line_end: z.string().optional().default("alt+e").describe("Move to end of visual line in input"), - input_select_visual_line_home: z - .string() - .optional() - .default("alt+shift+a") - .describe("Select to start of visual line in input"), - input_select_visual_line_end: z - .string() - .optional() - .default("alt+shift+e") - .describe("Select to end of visual line in input"), - input_buffer_home: z.string().optional().default("home").describe("Move to start of buffer in input"), - input_buffer_end: z.string().optional().default("end").describe("Move to end of buffer in input"), - input_select_buffer_home: z - .string() - .optional() - .default("shift+home") - .describe("Select to start of buffer in input"), - input_select_buffer_end: z.string().optional().default("shift+end").describe("Select to end of buffer in input"), - input_delete_line: z.string().optional().default("ctrl+shift+d").describe("Delete line in input"), - input_delete_to_line_end: z.string().optional().default("ctrl+k").describe("Delete to end of line in input"), - input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"), - input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"), - input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"), - input_undo: z - .string() - .optional() - // On Windows prepend ctrl+z since terminal_suspend releases the binding. - .default(process.platform === "win32" ? "ctrl+z,ctrl+-,super+z" : "ctrl+-,super+z") - .describe("Undo in input"), - input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"), - input_word_forward: z - .string() - .optional() - .default("alt+f,alt+right,ctrl+right") - .describe("Move word forward in input"), - input_word_backward: z - .string() - .optional() - .default("alt+b,alt+left,ctrl+left") - .describe("Move word backward in input"), - input_select_word_forward: z - .string() - .optional() - .default("alt+shift+f,alt+shift+right") - .describe("Select word forward in input"), - input_select_word_backward: z - .string() - .optional() - .default("alt+shift+b,alt+shift+left") - .describe("Select word backward in input"), - input_delete_word_forward: z - .string() - .optional() - .default("alt+d,alt+delete,ctrl+delete") - .describe("Delete word forward in input"), - input_delete_word_backward: z - .string() - .optional() - .default("ctrl+w,ctrl+backspace,alt+backspace") - .describe("Delete word backward in input"), - history_previous: z.string().optional().default("up").describe("Previous history item"), - history_next: z.string().optional().default("down").describe("Next history item"), - session_child_first: z.string().optional().default("down").describe("Go to first child session"), - session_child_cycle: z.string().optional().default("right").describe("Go to next child session"), - session_child_cycle_reverse: z.string().optional().default("left").describe("Go to previous child session"), - session_parent: z.string().optional().default("up").describe("Go to parent session"), - terminal_suspend: z - .string() - .optional() - .default("ctrl+z") - .transform((v) => (process.platform === "win32" ? "none" : v)) - .describe("Suspend terminal"), - terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"), - tips_toggle: z.string().optional().default("h").describe("Toggle tips on home screen"), - plugin_manager: z.string().optional().default("none").describe("Open plugin manager dialog"), - display_thinking: z.string().optional().default("none").describe("Toggle thinking blocks visibility"), - }) - .strict() - .meta({ - ref: "KeybindsConfig", - }) +// Every keybind field has the same shape: an optional string with a default +// binding and a human description. `keybind()` keeps the declaration list +// below dense and readable. +const keybind = (value: string, description: string) => + Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(value))).annotate({ description }) + +// Windows prepends ctrl+z to the undo binding because `terminal_suspend` +// cannot consume ctrl+z on native Windows terminals (no POSIX suspend). +const inputUndoDefault = process.platform === "win32" ? "ctrl+z,ctrl+-,super+z" : "ctrl+-,super+z" + +const KeybindsSchema = Schema.Struct({ + leader: keybind("ctrl+x", "Leader key for keybind combinations"), + app_exit: keybind("ctrl+c,ctrl+d,q", "Exit the application"), + editor_open: keybind("e", "Open external editor"), + theme_list: keybind("t", "List available themes"), + sidebar_toggle: keybind("b", "Toggle sidebar"), + scrollbar_toggle: keybind("none", "Toggle session scrollbar"), + username_toggle: keybind("none", "Toggle username visibility"), + status_view: keybind("s", "View status"), + session_export: keybind("x", "Export session to editor"), + session_new: keybind("n", "Create a new session"), + session_list: keybind("l", "List all sessions"), + session_timeline: keybind("g", "Show session timeline"), + session_fork: keybind("none", "Fork session from message"), + session_rename: keybind("ctrl+r", "Rename session"), + session_delete: keybind("ctrl+d", "Delete session"), + stash_delete: keybind("ctrl+d", "Delete stash entry"), + model_provider_list: keybind("ctrl+a", "Open provider list from model dialog"), + model_favorite_toggle: keybind("ctrl+f", "Toggle model favorite status"), + session_share: keybind("none", "Share current session"), + session_unshare: keybind("none", "Unshare current session"), + session_interrupt: keybind("escape", "Interrupt current session"), + session_compact: keybind("c", "Compact the session"), + messages_page_up: keybind("pageup,ctrl+alt+b", "Scroll messages up by one page"), + messages_page_down: keybind("pagedown,ctrl+alt+f", "Scroll messages down by one page"), + messages_line_up: keybind("ctrl+alt+y", "Scroll messages up by one line"), + messages_line_down: keybind("ctrl+alt+e", "Scroll messages down by one line"), + messages_half_page_up: keybind("ctrl+alt+u", "Scroll messages up by half page"), + messages_half_page_down: keybind("ctrl+alt+d", "Scroll messages down by half page"), + messages_first: keybind("ctrl+g,home", "Navigate to first message"), + messages_last: keybind("ctrl+alt+g,end", "Navigate to last message"), + messages_next: keybind("none", "Navigate to next message"), + messages_previous: keybind("none", "Navigate to previous message"), + messages_last_user: keybind("none", "Navigate to last user message"), + messages_copy: keybind("y", "Copy message"), + messages_undo: keybind("u", "Undo message"), + messages_redo: keybind("r", "Redo message"), + messages_toggle_conceal: keybind("h", "Toggle code block concealment in messages"), + tool_details: keybind("none", "Toggle tool details visibility"), + model_list: keybind("m", "List available models"), + model_cycle_recent: keybind("f2", "Next recently used model"), + model_cycle_recent_reverse: keybind("shift+f2", "Previous recently used model"), + model_cycle_favorite: keybind("none", "Next favorite model"), + model_cycle_favorite_reverse: keybind("none", "Previous favorite model"), + command_list: keybind("ctrl+p", "List available commands"), + agent_list: keybind("a", "List agents"), + agent_cycle: keybind("tab", "Next agent"), + agent_cycle_reverse: keybind("shift+tab", "Previous agent"), + variant_cycle: keybind("ctrl+t", "Cycle model variants"), + variant_list: keybind("none", "List model variants"), + input_clear: keybind("ctrl+c", "Clear input field"), + input_paste: keybind("ctrl+v", "Paste from clipboard"), + input_submit: keybind("return", "Submit input"), + input_newline: keybind("shift+return,ctrl+return,alt+return,ctrl+j", "Insert newline in input"), + input_move_left: keybind("left,ctrl+b", "Move cursor left in input"), + input_move_right: keybind("right,ctrl+f", "Move cursor right in input"), + input_move_up: keybind("up", "Move cursor up in input"), + input_move_down: keybind("down", "Move cursor down in input"), + input_select_left: keybind("shift+left", "Select left in input"), + input_select_right: keybind("shift+right", "Select right in input"), + input_select_up: keybind("shift+up", "Select up in input"), + input_select_down: keybind("shift+down", "Select down in input"), + input_line_home: keybind("ctrl+a", "Move to start of line in input"), + input_line_end: keybind("ctrl+e", "Move to end of line in input"), + input_select_line_home: keybind("ctrl+shift+a", "Select to start of line in input"), + input_select_line_end: keybind("ctrl+shift+e", "Select to end of line in input"), + input_visual_line_home: keybind("alt+a", "Move to start of visual line in input"), + input_visual_line_end: keybind("alt+e", "Move to end of visual line in input"), + input_select_visual_line_home: keybind("alt+shift+a", "Select to start of visual line in input"), + input_select_visual_line_end: keybind("alt+shift+e", "Select to end of visual line in input"), + input_buffer_home: keybind("home", "Move to start of buffer in input"), + input_buffer_end: keybind("end", "Move to end of buffer in input"), + input_select_buffer_home: keybind("shift+home", "Select to start of buffer in input"), + input_select_buffer_end: keybind("shift+end", "Select to end of buffer in input"), + input_delete_line: keybind("ctrl+shift+d", "Delete line in input"), + input_delete_to_line_end: keybind("ctrl+k", "Delete to end of line in input"), + input_delete_to_line_start: keybind("ctrl+u", "Delete to start of line in input"), + input_backspace: keybind("backspace,shift+backspace", "Backspace in input"), + input_delete: keybind("ctrl+d,delete,shift+delete", "Delete character in input"), + input_undo: keybind(inputUndoDefault, "Undo in input"), + input_redo: keybind("ctrl+.,super+shift+z", "Redo in input"), + input_word_forward: keybind("alt+f,alt+right,ctrl+right", "Move word forward in input"), + input_word_backward: keybind("alt+b,alt+left,ctrl+left", "Move word backward in input"), + input_select_word_forward: keybind("alt+shift+f,alt+shift+right", "Select word forward in input"), + input_select_word_backward: keybind("alt+shift+b,alt+shift+left", "Select word backward in input"), + input_delete_word_forward: keybind("alt+d,alt+delete,ctrl+delete", "Delete word forward in input"), + input_delete_word_backward: keybind("ctrl+w,ctrl+backspace,alt+backspace", "Delete word backward in input"), + history_previous: keybind("up", "Previous history item"), + history_next: keybind("down", "Next history item"), + session_child_first: keybind("down", "Go to first child session"), + session_child_cycle: keybind("right", "Go to next child session"), + session_child_cycle_reverse: keybind("left", "Go to previous child session"), + session_parent: keybind("up", "Go to parent session"), + // `terminal_suspend` was formerly `.default("ctrl+z").transform((v) => win32 ? "none" : v)`, + // but `tui.ts` already forces the binding to "none" on win32 before calling + // `Keybinds.parse(...)`, so the schema-level transform was redundant. + terminal_suspend: keybind("ctrl+z", "Suspend terminal"), + terminal_title_toggle: keybind("none", "Toggle terminal title"), + tips_toggle: keybind("h", "Toggle tips on home screen"), + plugin_manager: keybind("none", "Open plugin manager dialog"), + display_thinking: keybind("none", "Toggle thinking blocks visibility"), +}).annotate({ identifier: "KeybindsConfig" }) + +export type Keybinds = Schema.Schema.Type + +// Consumers access `Keybinds.shape` and `Keybinds.shape.X.parse(undefined)`, +// which requires the runtime type to be a ZodObject, not just ZodType. Every +// field is `string().optional().default(...)` at runtime, so widen to that. +export const Keybinds = zod(KeybindsSchema) as unknown as z.ZodObject< + Record>> +> From 357301991624ae6c361350c1c1aca222268a6d5d Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 22:31:21 -0400 Subject: [PATCH 171/335] fix(generate): make openapi output deterministic by formatting in-place (#23228) --- packages/opencode/src/cli/cmd/generate.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/generate.ts b/packages/opencode/src/cli/cmd/generate.ts index fad4514c81..0531d537c2 100644 --- a/packages/opencode/src/cli/cmd/generate.ts +++ b/packages/opencode/src/cli/cmd/generate.ts @@ -25,7 +25,19 @@ export const GenerateCommand = { ] } } - const json = JSON.stringify(specs, null, 2) + const raw = JSON.stringify(specs, null, 2) + + // Format through prettier so output is byte-identical to committed file + // regardless of whether ./script/format.ts runs afterward. + const prettier = await import("prettier") + const babel = await import("prettier/plugins/babel") + const estree = await import("prettier/plugins/estree") + const format = prettier.format ?? prettier.default?.format + const json = await format(raw, { + parser: "json", + plugins: [babel.default ?? babel, estree.default ?? estree], + printWidth: 120, + }) // Wait for stdout to finish writing before process.exit() is called await new Promise((resolve, reject) => { From 24fb9b1296d7bb5942ef5690ee3a806856b18dae Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 22:53:19 -0400 Subject: [PATCH 172/335] fix: stop rewriting dev during release publish (#22982) --- packages/opencode/script/publish.ts | 29 +++++++--- packages/plugin/script/publish.ts | 12 +++- packages/sdk/js/script/publish.ts | 37 ++++++++----- script/publish.ts | 86 ++++++++++++++++++++--------- script/version.ts | 6 +- 5 files changed, 120 insertions(+), 50 deletions(-) diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts index 9c4b8f187d..b444106a1b 100755 --- a/packages/opencode/script/publish.ts +++ b/packages/opencode/script/publish.ts @@ -7,6 +7,20 @@ import { fileURLToPath } from "url" const dir = fileURLToPath(new URL("..", import.meta.url)) process.chdir(dir) +async function published(name: string, version: string) { + return (await $`npm view ${name}@${version} version`.nothrow()).exitCode === 0 +} + +async function publish(dir: string, name: string, version: string) { + if (await published(name, version)) { + console.log(`already published ${name}@${version}`) + return + } + if (process.platform !== "win32") await $`chmod -R 755 .`.cwd(dir) + await $`bun pm pack`.cwd(dir) + await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir) +} + const binaries: Record = {} for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })) { const pkg = await Bun.file(`./dist/${filepath}`).json() @@ -40,14 +54,10 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write( ) const tasks = Object.entries(binaries).map(async ([name]) => { - if (process.platform !== "win32") { - await $`chmod -R 755 .`.cwd(`./dist/${name}`) - } - await $`bun pm pack`.cwd(`./dist/${name}`) - await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(`./dist/${name}`) + await publish(`./dist/${name}`, name, binaries[name]) }) await Promise.all(tasks) -await $`cd ./dist/${pkg.name} && bun pm pack && npm publish *.tgz --access public --tag ${Script.channel}` +await publish(`./dist/${pkg.name}`, `${pkg.name}-ai`, version) const image = "ghcr.io/anomalyco/opencode" const platforms = "linux/amd64,linux/arm64" @@ -104,6 +114,7 @@ if (!Script.preview) { await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild) await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO` await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO` + if ((await $`cd ./dist/aur-${pkg} && git diff --cached --quiet`.nothrow()).exitCode === 0) break await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${Script.version}"` await $`cd ./dist/aur-${pkg} && git push` break @@ -176,6 +187,8 @@ if (!Script.preview) { await $`git clone ${tap} ./dist/homebrew-tap` await Bun.file("./dist/homebrew-tap/opencode.rb").write(homebrewFormula) await $`cd ./dist/homebrew-tap && git add opencode.rb` - await $`cd ./dist/homebrew-tap && git commit -m "Update to v${Script.version}"` - await $`cd ./dist/homebrew-tap && git push` + if ((await $`cd ./dist/homebrew-tap && git diff --cached --quiet`.nothrow()).exitCode !== 0) { + await $`cd ./dist/homebrew-tap && git commit -m "Update to v${Script.version}"` + await $`cd ./dist/homebrew-tap && git push` + } } diff --git a/packages/plugin/script/publish.ts b/packages/plugin/script/publish.ts index d2fe49f23c..de129918cd 100755 --- a/packages/plugin/script/publish.ts +++ b/packages/plugin/script/publish.ts @@ -6,9 +6,19 @@ import { fileURLToPath } from "url" const dir = fileURLToPath(new URL("..", import.meta.url)) process.chdir(dir) +async function published(name: string, version: string) { + return (await $`npm view ${name}@${version} version`.nothrow()).exitCode === 0 +} + await $`bun tsc` -const pkg = await import("../package.json").then((m) => m.default) +const pkg = await import("../package.json").then( + (m) => m.default as { name: string; version: string; exports: Record }, +) const original = JSON.parse(JSON.stringify(pkg)) +if (await published(pkg.name, pkg.version)) { + console.log(`already published ${pkg.name}@${pkg.version}`) + process.exit(0) +} for (const [key, value] of Object.entries(pkg.exports)) { const file = value.replace("./src/", "./dist/").replace(".ts", "") // @ts-ignore diff --git a/packages/sdk/js/script/publish.ts b/packages/sdk/js/script/publish.ts index ea5c5d634b..b5e1211fc4 100755 --- a/packages/sdk/js/script/publish.ts +++ b/packages/sdk/js/script/publish.ts @@ -7,24 +7,35 @@ import { fileURLToPath } from "url" const dir = fileURLToPath(new URL("..", import.meta.url)) process.chdir(dir) +async function published(name: string, version: string) { + return (await $`npm view ${name}@${version} version`.nothrow()).exitCode === 0 +} + const pkg = (await import("../package.json").then((m) => m.default)) as { - exports: Record + name: string + version: string + exports: Record } const original = JSON.parse(JSON.stringify(pkg)) -function transformExports(exports: Record) { - for (const [key, value] of Object.entries(exports)) { - if (typeof value === "object" && value !== null) { - transformExports(value as Record) - } else if (typeof value === "string") { - const file = value.replace("./src/", "./dist/").replace(".ts", "") - exports[key] = { - import: file + ".js", - types: file + ".d.ts", +function transformExports(exports: Record) { + return Object.fromEntries( + Object.entries(exports).map(([key, value]) => { + if (typeof value === "string") { + const file = value.replace("./src/", "./dist/").replace(".ts", "") + return [key, { import: file + ".js", types: file + ".d.ts" }] } - } - } + if (typeof value === "object" && value !== null && !Array.isArray(value)) { + return [key, transformExports(value)] + } + return [key, value] + }), + ) } -transformExports(pkg.exports) +if (await published(pkg.name, pkg.version)) { + console.log(`already published ${pkg.name}@${pkg.version}`) + process.exit(0) +} +pkg.exports = transformExports(pkg.exports) await Bun.write("package.json", JSON.stringify(pkg, null, 2)) await $`bun pm pack` await $`npm publish *.tgz --tag ${Script.channel} --access public` diff --git a/script/publish.ts b/script/publish.ts index ac0af9c4e6..6cd244e0e6 100755 --- a/script/publish.ts +++ b/script/publish.ts @@ -6,43 +6,58 @@ import { fileURLToPath } from "url" console.log("=== publishing ===\n") +const tag = `v${Script.version}` + const pkgjsons = await Array.fromAsync( new Bun.Glob("**/package.json").scan({ absolute: true, }), ).then((arr) => arr.filter((x) => !x.includes("node_modules") && !x.includes("dist"))) -for (const file of pkgjsons) { - let pkg = await Bun.file(file).text() - pkg = pkg.replaceAll(/"version": "[^"]+"/g, `"version": "${Script.version}"`) - console.log("updated:", file) - await Bun.file(file).write(pkg) +const extensionToml = fileURLToPath(new URL("../packages/extensions/zed/extension.toml", import.meta.url)) + +async function hasChanges() { + return (await $`git diff --quiet && git diff --cached --quiet`.nothrow()).exitCode !== 0 } -const extensionToml = fileURLToPath(new URL("../packages/extensions/zed/extension.toml", import.meta.url)) -let toml = await Bun.file(extensionToml).text() -toml = toml.replace(/^version = "[^"]+"/m, `version = "${Script.version}"`) -toml = toml.replaceAll(/releases\/download\/v[^/]+\//g, `releases/download/v${Script.version}/`) -console.log("updated:", extensionToml) -await Bun.file(extensionToml).write(toml) +async function releaseTagExists() { + return (await $`git rev-parse -q --verify refs/tags/${tag}`.nothrow()).exitCode === 0 +} -await $`bun install` -await import(`../packages/sdk/js/script/build.ts`) - -if (Script.release) { - if (!Script.preview) { - await $`git commit -am "release: v${Script.version}"` - await $`git tag v${Script.version}` - await $`git fetch origin` - await $`git cherry-pick HEAD..origin/dev`.nothrow() - await $`git push origin HEAD --tags --no-verify --force-with-lease` - await new Promise((resolve) => setTimeout(resolve, 5_000)) +async function prepareReleaseFiles() { + for (const file of pkgjsons) { + let pkg = await Bun.file(file).text() + pkg = pkg.replaceAll(/"version": "[^"]+"/g, `"version": "${Script.version}"`) + console.log("updated:", file) + await Bun.file(file).write(pkg) } - await import(`../packages/desktop/scripts/finalize-latest-json.ts`) - await import(`../packages/desktop-electron/scripts/finalize-latest-yml.ts`) + let toml = await Bun.file(extensionToml).text() + toml = toml.replace(/^version = "[^"]+"/m, `version = "${Script.version}"`) + toml = toml.replaceAll(/releases\/download\/v[^/]+\//g, `releases/download/v${Script.version}/`) + console.log("updated:", extensionToml) + await Bun.file(extensionToml).write(toml) - await $`gh release edit v${Script.version} --draft=false --repo ${process.env.GH_REPO}` + await $`bun install` + await $`./packages/sdk/js/script/build.ts` +} + +if (Script.release && !Script.preview) { + await $`git fetch origin --tags` + await $`git switch --detach` +} + +await prepareReleaseFiles() + +if (Script.release && !Script.preview) { + if (await releaseTagExists()) { + console.log(`release tag ${tag} already exists, skipping tag creation`) + } else { + await $`git commit -am "release: ${tag}"` + await $`git tag ${tag}` + await $`git push origin refs/tags/${tag} --no-verify` + await new Promise((resolve) => setTimeout(resolve, 5_000)) + } } console.log("\n=== cli ===\n") @@ -54,5 +69,26 @@ await import(`../packages/sdk/js/script/publish.ts`) console.log("\n=== plugin ===\n") await import(`../packages/plugin/script/publish.ts`) +if (Script.release) { + await import(`../packages/desktop/scripts/finalize-latest-json.ts`) + await import(`../packages/desktop-electron/scripts/finalize-latest-yml.ts`) +} + +if (Script.release && !Script.preview) { + await $`git fetch origin` + await $`git checkout -B dev origin/dev` + await prepareReleaseFiles() + if (await hasChanges()) { + await $`git commit -am "sync release versions for v${Script.version}"` + await $`git push origin HEAD:dev --no-verify` + } else { + console.log(`dev already synced for ${tag}`) + } +} + +if (Script.release) { + await $`gh release edit ${tag} --draft=false --repo ${process.env.GH_REPO}` +} + const dir = fileURLToPath(new URL("..", import.meta.url)) process.chdir(dir) diff --git a/script/version.ts b/script/version.ts index 3ca4f661d2..c1ad021b69 100755 --- a/script/version.ts +++ b/script/version.ts @@ -4,9 +4,9 @@ import { Script } from "@opencode-ai/script" import { $ } from "bun" const output = [`version=${Script.version}`] +const sha = process.env.GITHUB_SHA ?? (await $`git rev-parse HEAD`.text()).trim() if (!Script.preview) { - const sha = process.env.GITHUB_SHA ?? (await $`git rev-parse HEAD`.text()).trim() await $`bun script/changelog.ts --to ${sha}`.cwd(process.cwd()) const file = `${process.cwd()}/UPCOMING_CHANGELOG.md` const body = await Bun.file(file) @@ -15,12 +15,12 @@ if (!Script.preview) { const dir = process.env.RUNNER_TEMP ?? "/tmp" const notesFile = `${dir}/opencode-release-notes.txt` await Bun.write(notesFile, body) - await $`gh release create v${Script.version} -d --title "v${Script.version}" --notes-file ${notesFile}` + await $`gh release create v${Script.version} -d --target ${sha} --title "v${Script.version}" --notes-file ${notesFile}` const release = await $`gh release view v${Script.version} --json tagName,databaseId`.json() output.push(`release=${release.databaseId}`) output.push(`tag=${release.tagName}`) } else if (Script.channel === "beta") { - await $`gh release create v${Script.version} -d --title "v${Script.version}" --repo ${process.env.GH_REPO}` + await $`gh release create v${Script.version} -d --target ${sha} --title "v${Script.version}" --repo ${process.env.GH_REPO}` const release = await $`gh release view v${Script.version} --json tagName,databaseId --repo ${process.env.GH_REPO}`.json() output.push(`release=${release.databaseId}`) From 471b9f4dc443869795e0732c668e366150951d36 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 23:04:16 -0400 Subject: [PATCH 173/335] refactor: use InstanceState context in worktree cleanup paths (#23019) --- packages/opencode/src/worktree/index.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index aa1dc2f8f1..bbebeaa496 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -365,13 +365,14 @@ export const layer: Layer.Layer< } const remove = Effect.fn("Worktree.remove")(function* (input: RemoveInput) { - if (Instance.project.vcs !== "git") { + const ctx = yield* InstanceState.context + if (ctx.project.vcs !== "git") { throw new NotGitError({ message: "Worktrees are only supported for git projects" }) } const directory = yield* canonical(input.directory) - const list = yield* git(["worktree", "list", "--porcelain"], { cwd: Instance.worktree }) + const list = yield* git(["worktree", "list", "--porcelain"], { cwd: ctx.worktree }) if (list.code !== 0) { throw new RemoveFailedError({ message: list.stderr || list.text || "Failed to read git worktrees" }) } @@ -389,9 +390,9 @@ export const layer: Layer.Layer< } yield* stopFsmonitor(entry.path) - const removed = yield* git(["worktree", "remove", "--force", entry.path], { cwd: Instance.worktree }) + const removed = yield* git(["worktree", "remove", "--force", entry.path], { cwd: ctx.worktree }) if (removed.code !== 0) { - const next = yield* git(["worktree", "list", "--porcelain"], { cwd: Instance.worktree }) + const next = yield* git(["worktree", "list", "--porcelain"], { cwd: ctx.worktree }) if (next.code !== 0) { throw new RemoveFailedError({ message: removed.stderr || removed.text || next.stderr || next.text || "Failed to remove git worktree", @@ -408,7 +409,7 @@ export const layer: Layer.Layer< const branch = entry.branch?.replace(/^refs\/heads\//, "") if (branch) { - const deleted = yield* git(["branch", "-D", branch], { cwd: Instance.worktree }) + const deleted = yield* git(["branch", "-D", branch], { cwd: ctx.worktree }) if (deleted.code !== 0) { throw new RemoveFailedError({ message: deleted.stderr || deleted.text || "Failed to delete worktree branch", @@ -498,17 +499,18 @@ export const layer: Layer.Layer< }) const reset = Effect.fn("Worktree.reset")(function* (input: ResetInput) { - if (Instance.project.vcs !== "git") { + const ctx = yield* InstanceState.context + if (ctx.project.vcs !== "git") { throw new NotGitError({ message: "Worktrees are only supported for git projects" }) } const directory = yield* canonical(input.directory) - const primary = yield* canonical(Instance.worktree) + const primary = yield* canonical(ctx.worktree) if (directory === primary) { throw new ResetFailedError({ message: "Cannot reset the primary workspace" }) } - const list = yield* git(["worktree", "list", "--porcelain"], { cwd: Instance.worktree }) + const list = yield* git(["worktree", "list", "--porcelain"], { cwd: ctx.worktree }) if (list.code !== 0) { throw new ResetFailedError({ message: list.stderr || list.text || "Failed to read git worktrees" }) } @@ -520,7 +522,7 @@ export const layer: Layer.Layer< const worktreePath = entry.path - const base = yield* gitSvc.defaultBranch(Instance.worktree) + const base = yield* gitSvc.defaultBranch(ctx.worktree) if (!base) { throw new ResetFailedError({ message: "Default branch not found" }) } @@ -531,7 +533,7 @@ export const layer: Layer.Layer< const branch = base.ref.slice(sep + 1) yield* gitExpect( ["fetch", remote, branch], - { cwd: Instance.worktree }, + { cwd: ctx.worktree }, (r) => new ResetFailedError({ message: r.stderr || r.text || `Failed to fetch ${base.ref}` }), ) } @@ -574,7 +576,7 @@ export const layer: Layer.Layer< throw new ResetFailedError({ message: `Worktree reset left local changes:\n${status.text.trim()}` }) } - yield* runStartScripts(worktreePath, { projectID: Instance.project.id }).pipe( + yield* runStartScripts(worktreePath, { projectID: ctx.project.id }).pipe( Effect.catchCause((cause) => Effect.sync(() => log.error("worktree start task failed", { cause }))), Effect.forkIn(scope), ) From a6a4350d1032da8b1ae1ae1a51f11b7a0844fcff Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 23:05:06 -0400 Subject: [PATCH 174/335] refactor(config): migrate permission.ts Info to Effect Schema (#23231) --- packages/opencode/src/config/permission.ts | 85 ++++++++++++---------- 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/packages/opencode/src/config/permission.ts b/packages/opencode/src/config/permission.ts index 7cfbaec01f..eb3ac412bd 100644 --- a/packages/opencode/src/config/permission.ts +++ b/packages/opencode/src/config/permission.ts @@ -1,16 +1,8 @@ export * as ConfigPermission from "./permission" import { Schema } from "effect" -import z from "zod" -import { zod } from "@/util/effect-zod" +import { zod, ZodPreprocess } from "@/util/effect-zod" import { withStatics } from "@/util/schema" -const permissionPreprocess = (val: unknown) => { - if (typeof val === "object" && val !== null && !Array.isArray(val)) { - return { __originalKeys: globalThis.Object.keys(val), ...val } - } - return val -} - export const Action = Schema.Literals(["ask", "allow", "deny"]) .annotate({ identifier: "PermissionActionConfig" }) .pipe(withStatics((s) => ({ zod: zod(s) }))) @@ -26,6 +18,48 @@ export const Rule = Schema.Union([Action, Object]) .pipe(withStatics((s) => ({ zod: zod(s) }))) export type Rule = Schema.Schema.Type +// Captures the user's original property insertion order before Schema.Struct +// canonicalises the object. See the `ZodPreprocess` comment in +// `util/effect-zod.ts` for the full rationale — in short: rule precedence is +// encoded in JSON key order (`evaluate.ts` uses `findLast`, so later keys win) +// and `Schema.StructWithRest` would otherwise drop that order. +const permissionPreprocess = (val: unknown) => { + if (typeof val === "object" && val !== null && !Array.isArray(val)) { + return { __originalKeys: globalThis.Object.keys(val), ...val } + } + return val +} + +const ObjectShape = Schema.StructWithRest( + Schema.Struct({ + __originalKeys: Schema.optional(Schema.mutable(Schema.Array(Schema.String))), + read: Schema.optional(Rule), + edit: Schema.optional(Rule), + glob: Schema.optional(Rule), + grep: Schema.optional(Rule), + list: Schema.optional(Rule), + bash: Schema.optional(Rule), + task: Schema.optional(Rule), + external_directory: Schema.optional(Rule), + todowrite: Schema.optional(Action), + question: Schema.optional(Action), + webfetch: Schema.optional(Action), + websearch: Schema.optional(Action), + codesearch: Schema.optional(Action), + lsp: Schema.optional(Rule), + doom_loop: Schema.optional(Action), + skill: Schema.optional(Rule), + }), + [Schema.Record(Schema.String, Rule)], +) + +const InnerSchema = Schema.Union([ObjectShape, Action]).annotate({ + [ZodPreprocess]: permissionPreprocess, +}) + +// Post-parse: drop the __originalKeys metadata and rebuild the rule map in the +// user's original insertion order. A plain string input (the Action branch of +// the union) becomes `{ "*": action }`. const transform = (x: unknown): Record => { if (typeof x === "string") return { "*": x as Action } const obj = x as { __originalKeys?: string[] } & Record @@ -38,34 +72,7 @@ const transform = (x: unknown): Record => { return result } -export const Info = z - .preprocess( - permissionPreprocess, - z - .object({ - __originalKeys: z.string().array().optional(), - read: Rule.zod.optional(), - edit: Rule.zod.optional(), - glob: Rule.zod.optional(), - grep: Rule.zod.optional(), - list: Rule.zod.optional(), - bash: Rule.zod.optional(), - task: Rule.zod.optional(), - external_directory: Rule.zod.optional(), - todowrite: Action.zod.optional(), - question: Action.zod.optional(), - webfetch: Action.zod.optional(), - websearch: Action.zod.optional(), - codesearch: Action.zod.optional(), - lsp: Rule.zod.optional(), - doom_loop: Action.zod.optional(), - skill: Rule.zod.optional(), - }) - .catchall(Rule.zod) - .or(Action.zod), - ) +export const Info = zod(InnerSchema) .transform(transform) - .meta({ - ref: "PermissionConfig", - }) -export type Info = z.infer + .meta({ ref: "PermissionConfig" }) +export type Info = Record From 9f7bd0246c14620758865513ceddfe0a768bec2e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 18 Apr 2026 03:05:59 +0000 Subject: [PATCH 175/335] chore: generate --- packages/opencode/src/config/permission.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/opencode/src/config/permission.ts b/packages/opencode/src/config/permission.ts index eb3ac412bd..d4883ed8c1 100644 --- a/packages/opencode/src/config/permission.ts +++ b/packages/opencode/src/config/permission.ts @@ -72,7 +72,5 @@ const transform = (x: unknown): Record => { return result } -export const Info = zod(InnerSchema) - .transform(transform) - .meta({ ref: "PermissionConfig" }) +export const Info = zod(InnerSchema).transform(transform).meta({ ref: "PermissionConfig" }) export type Info = Record From 2793502db20ffc88c18fdec91133ac157d470461 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 23:16:24 -0400 Subject: [PATCH 176/335] refactor(config): migrate agent.ts Info to Effect Schema (#23237) --- packages/opencode/src/config/agent.ts | 179 ++++++++++++++------------ 1 file changed, 98 insertions(+), 81 deletions(-) diff --git a/packages/opencode/src/config/agent.ts b/packages/opencode/src/config/agent.ts index 9053b19fc1..32dbc74102 100644 --- a/packages/opencode/src/config/agent.ts +++ b/packages/opencode/src/config/agent.ts @@ -1,10 +1,12 @@ export * as ConfigAgent from "./agent" -import { Log } from "../util" +import { Schema } from "effect" import z from "zod" +import { Bus } from "@/bus" +import { zod, ZodOverride } from "@/util/effect-zod" +import { Log } from "../util" import { NamedError } from "@opencode-ai/shared/util/error" import { Glob } from "@opencode-ai/shared/util/glob" -import { Bus } from "@/bus" import { configEntryNameFromPath } from "./entry-name" import { InvalidError } from "./error" import * as ConfigMarkdown from "./markdown" @@ -13,89 +15,104 @@ import { ConfigPermission } from "./permission" const log = Log.create({ service: "config" }) -export const Info = z - .object({ - model: ConfigModelID.zod.optional(), - variant: z - .string() - .optional() - .describe("Default model variant for this agent (applies only when using the agent's configured model)."), - temperature: z.number().optional(), - top_p: z.number().optional(), - prompt: z.string().optional(), - tools: z.record(z.string(), z.boolean()).optional().describe("@deprecated Use 'permission' field instead"), - disable: z.boolean().optional(), - description: z.string().optional().describe("Description of when to use the agent"), - mode: z.enum(["subagent", "primary", "all"]).optional(), - hidden: z - .boolean() - .optional() - .describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"), - options: z.record(z.string(), z.any()).optional(), - color: z - .union([ - z.string().regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format"), - z.enum(["primary", "secondary", "accent", "success", "warning", "error", "info"]), - ]) - .optional() - .describe("Hex color code (e.g., #FF5733) or theme color (e.g., primary)"), - steps: z - .number() - .int() - .positive() - .optional() - .describe("Maximum number of agentic iterations before forcing text-only response"), - maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."), - permission: ConfigPermission.Info.optional(), - }) - .catchall(z.any()) - .transform((agent, _ctx) => { - const knownKeys = new Set([ - "name", - "model", - "variant", - "prompt", - "description", - "temperature", - "top_p", - "mode", - "hidden", - "color", - "steps", - "maxSteps", - "options", - "permission", - "disable", - "tools", - ]) +const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)) - const options: Record = { ...agent.options } - for (const [key, value] of Object.entries(agent)) { - if (!knownKeys.has(key)) options[key] = value +const Color = Schema.Union([ + Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), + Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), +]) + +// ConfigPermission.Info is a zod schema (its `.preprocess(...).transform(...)` +// shape lives outside the Effect Schema type system), so the walker reaches it +// via ZodOverride rather than a pure Schema reference. This preserves the +// `$ref: PermissionConfig` emitted in openapi.json. +const PermissionRef = Schema.Any.annotate({ [ZodOverride]: ConfigPermission.Info }) + +const AgentSchema = Schema.StructWithRest( + Schema.Struct({ + model: Schema.optional(ConfigModelID), + variant: Schema.optional(Schema.String).annotate({ + description: "Default model variant for this agent (applies only when using the agent's configured model).", + }), + temperature: Schema.optional(Schema.Number), + top_p: Schema.optional(Schema.Number), + prompt: Schema.optional(Schema.String), + tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)).annotate({ + description: "@deprecated Use 'permission' field instead", + }), + disable: Schema.optional(Schema.Boolean), + description: Schema.optional(Schema.String).annotate({ description: "Description of when to use the agent" }), + mode: Schema.optional(Schema.Literals(["subagent", "primary", "all"])), + hidden: Schema.optional(Schema.Boolean).annotate({ + description: "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)", + }), + options: Schema.optional(Schema.Record(Schema.String, Schema.Any)), + color: Schema.optional(Color).annotate({ + description: "Hex color code (e.g., #FF5733) or theme color (e.g., primary)", + }), + steps: Schema.optional(PositiveInt).annotate({ + description: "Maximum number of agentic iterations before forcing text-only response", + }), + maxSteps: Schema.optional(PositiveInt).annotate({ description: "@deprecated Use 'steps' field instead." }), + permission: Schema.optional(PermissionRef), + }), + [Schema.Record(Schema.String, Schema.Any)], +) + +const KNOWN_KEYS = new Set([ + "name", + "model", + "variant", + "prompt", + "description", + "temperature", + "top_p", + "mode", + "hidden", + "color", + "steps", + "maxSteps", + "options", + "permission", + "disable", + "tools", +]) + +// Post-parse normalisation: +// - Promote any unknown-but-present keys into `options` so they survive the +// round-trip in a well-known field. +// - Translate the deprecated `tools: { name: boolean }` map into the new +// `permission` shape (write-adjacent tools collapse into `permission.edit`). +// - Coalesce `steps ?? maxSteps` so downstream can ignore the deprecated alias. +const normalize = (agent: z.infer) => { + const options: Record = { ...agent.options } + for (const [key, value] of Object.entries(agent)) { + if (!KNOWN_KEYS.has(key)) options[key] = value + } + + const permission: ConfigPermission.Info = {} + for (const [tool, enabled] of Object.entries(agent.tools ?? {})) { + const action = enabled ? "allow" : "deny" + if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") { + permission.edit = action + continue } + permission[tool] = action + } + globalThis.Object.assign(permission, agent.permission) - const permission: ConfigPermission.Info = {} - for (const [tool, enabled] of Object.entries(agent.tools ?? {})) { - const action = enabled ? "allow" : "deny" - if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") { - permission.edit = action - continue - } - permission[tool] = action - } - Object.assign(permission, agent.permission) + return { ...agent, options, permission, steps: agent.steps ?? agent.maxSteps } +} - const steps = agent.steps ?? agent.maxSteps - - return { ...agent, options, permission, steps } as typeof agent & { - options?: Record - permission?: ConfigPermission.Info - steps?: number - } - }) - .meta({ - ref: "AgentConfig", - }) +export const Info = zod(AgentSchema) + .transform(normalize) + .meta({ ref: "AgentConfig" }) as unknown as z.ZodType< + Omit>>, "options" | "permission" | "steps"> & { + options?: Record + permission?: ConfigPermission.Info + steps?: number + } +> export type Info = z.infer export async function load(dir: string) { From 8a1e85d0c827b138a3af94f233a343cf8fbcccf2 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 18 Apr 2026 03:17:28 +0000 Subject: [PATCH 177/335] chore: generate --- packages/opencode/src/config/agent.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/opencode/src/config/agent.ts b/packages/opencode/src/config/agent.ts index 32dbc74102..1469522d98 100644 --- a/packages/opencode/src/config/agent.ts +++ b/packages/opencode/src/config/agent.ts @@ -104,9 +104,7 @@ const normalize = (agent: z.infer) => { return { ...agent, options, permission, steps: agent.steps ?? agent.maxSteps } } -export const Info = zod(AgentSchema) - .transform(normalize) - .meta({ ref: "AgentConfig" }) as unknown as z.ZodType< +export const Info = zod(AgentSchema).transform(normalize).meta({ ref: "AgentConfig" }) as unknown as z.ZodType< Omit>>, "options" | "permission" | "steps"> & { options?: Record permission?: ConfigPermission.Info From c0eab9e44285596066555688057335cd58a1f84e Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Sat, 18 Apr 2026 05:31:38 +0200 Subject: [PATCH 178/335] fix(desktop): adjust ui tool diff sticky header offset (#23149) --- packages/ui/src/components/message-part.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index a47ff18045..9c0c90c000 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -1269,7 +1269,7 @@ function ToolFileAccordion(props: { path: string; actions?: JSX.Element; childre @@ -2061,7 +2061,7 @@ ToolRegistry.register({ setExpanded(Array.isArray(value) ? value : value ? [value] : [])} > From 23f31475e7e8a5dce16b8095a45e954774ea65ca Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 23:44:35 -0400 Subject: [PATCH 179/335] refactor(config): migrate config.ts root Info to Effect Schema (#23241) --- packages/opencode/src/config/config.ts | 317 ++++++++++++++----------- 1 file changed, 174 insertions(+), 143 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 179c6a6093..248351e1a5 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -21,9 +21,10 @@ import { isRecord } from "@/util/record" import type { ConsoleState } from "./console-state" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { InstanceState } from "@/effect" -import { Context, Duration, Effect, Exit, Fiber, Layer, Option } from "effect" +import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect" import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" import { InstanceRef } from "@/effect/instance-ref" +import { zod, ZodOverride } from "@/util/effect-zod" import { ConfigAgent } from "./agent" import { ConfigCommand } from "./command" import { ConfigFormatter } from "./formatter" @@ -79,152 +80,182 @@ export const Server = ConfigServer.Server.zod export const Layout = ConfigLayout.Layout.zod export type Layout = ConfigLayout.Layout -export const Info = z - .object({ - $schema: z.string().optional().describe("JSON schema reference for configuration validation"), - logLevel: Log.Level.optional().describe("Log level"), - server: Server.optional().describe("Server configuration for opencode serve and web commands"), - command: z - .record(z.string(), ConfigCommand.Info.zod) - .optional() - .describe("Command configuration, see https://opencode.ai/docs/commands"), - skills: ConfigSkills.Info.zod.optional().describe("Additional skill folder paths"), - watcher: z - .object({ - ignore: z.array(z.string()).optional(), - }) - .optional(), - snapshot: z - .boolean() - .optional() - .describe( - "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.", - ), - // User-facing plugin config is stored as Specs; provenance gets attached later while configs are merged. - plugin: ConfigPlugin.Spec.zod.array().optional(), - share: z - .enum(["manual", "auto", "disabled"]) - .optional() - .describe( - "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing", - ), - autoshare: z - .boolean() - .optional() - .describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"), - autoupdate: z - .union([z.boolean(), z.literal("notify")]) - .optional() - .describe( - "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications", - ), - disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"), - enabled_providers: z - .array(z.string()) - .optional() - .describe("When set, ONLY these providers will be enabled. All other providers will be ignored"), - model: ConfigModelID.zod.describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(), - small_model: ConfigModelID.zod - .describe("Small model to use for tasks like title generation in the format of provider/model") - .optional(), - default_agent: z - .string() - .optional() - .describe( - "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.", - ), - username: z.string().optional().describe("Custom username to display in conversations instead of system username"), - mode: z - .object({ - build: ConfigAgent.Info.optional(), - plan: ConfigAgent.Info.optional(), - }) - .catchall(ConfigAgent.Info) - .optional() - .describe("@deprecated Use `agent` field instead."), - agent: z - .object({ +// Schemas that still live at the zod layer (have .transform / .preprocess / +// .meta not expressible in current Effect Schema) get referenced via a +// ZodOverride-annotated Schema.Any. Walker sees the annotation and emits the +// exact zod directly, preserving component $refs. +const AgentRef = Schema.Any.annotate({ [ZodOverride]: ConfigAgent.Info }) +const PermissionRef = Schema.Any.annotate({ [ZodOverride]: ConfigPermission.Info }) +const LogLevelRef = Schema.Any.annotate({ [ZodOverride]: Log.Level }) + +const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)) +const NonNegativeInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) + +const InfoSchema = Schema.Struct({ + $schema: Schema.optional(Schema.String).annotate({ + description: "JSON schema reference for configuration validation", + }), + logLevel: Schema.optional(LogLevelRef).annotate({ description: "Log level" }), + server: Schema.optional(ConfigServer.Server).annotate({ + description: "Server configuration for opencode serve and web commands", + }), + command: Schema.optional(Schema.Record(Schema.String, ConfigCommand.Info)).annotate({ + description: "Command configuration, see https://opencode.ai/docs/commands", + }), + skills: Schema.optional(ConfigSkills.Info).annotate({ description: "Additional skill folder paths" }), + watcher: Schema.optional( + Schema.Struct({ + ignore: Schema.optional(Schema.mutable(Schema.Array(Schema.String))), + }), + ), + snapshot: Schema.optional(Schema.Boolean).annotate({ + description: + "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.", + }), + // User-facing plugin config is stored as Specs; provenance gets attached later while configs are merged. + plugin: Schema.optional(Schema.mutable(Schema.Array(ConfigPlugin.Spec))), + share: Schema.optional(Schema.Literals(["manual", "auto", "disabled"])).annotate({ + description: + "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing", + }), + autoshare: Schema.optional(Schema.Boolean).annotate({ + description: "@deprecated Use 'share' field instead. Share newly created sessions automatically", + }), + autoupdate: Schema.optional(Schema.Union([Schema.Boolean, Schema.Literal("notify")])).annotate({ + description: + "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications", + }), + disabled_providers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({ + description: "Disable providers that are loaded automatically", + }), + enabled_providers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({ + description: "When set, ONLY these providers will be enabled. All other providers will be ignored", + }), + model: Schema.optional(ConfigModelID).annotate({ + description: "Model to use in the format of provider/model, eg anthropic/claude-2", + }), + small_model: Schema.optional(ConfigModelID).annotate({ + description: "Small model to use for tasks like title generation in the format of provider/model", + }), + default_agent: Schema.optional(Schema.String).annotate({ + description: + "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.", + }), + username: Schema.optional(Schema.String).annotate({ + description: "Custom username to display in conversations instead of system username", + }), + mode: Schema.optional( + Schema.StructWithRest( + Schema.Struct({ + build: Schema.optional(AgentRef), + plan: Schema.optional(AgentRef), + }), + [Schema.Record(Schema.String, AgentRef)], + ), + ).annotate({ description: "@deprecated Use `agent` field instead." }), + agent: Schema.optional( + Schema.StructWithRest( + Schema.Struct({ // primary - plan: ConfigAgent.Info.optional(), - build: ConfigAgent.Info.optional(), + plan: Schema.optional(AgentRef), + build: Schema.optional(AgentRef), // subagent - general: ConfigAgent.Info.optional(), - explore: ConfigAgent.Info.optional(), + general: Schema.optional(AgentRef), + explore: Schema.optional(AgentRef), // specialized - title: ConfigAgent.Info.optional(), - summary: ConfigAgent.Info.optional(), - compaction: ConfigAgent.Info.optional(), - }) - .catchall(ConfigAgent.Info) - .optional() - .describe("Agent configuration, see https://opencode.ai/docs/agents"), - provider: z - .record(z.string(), ConfigProvider.Info.zod) - .optional() - .describe("Custom provider configurations and model overrides"), - mcp: z - .record( - z.string(), - z.union([ - ConfigMCP.Info.zod, - z - .object({ - enabled: z.boolean(), - }) - .strict(), - ]), - ) - .optional() - .describe("MCP (Model Context Protocol) server configurations"), - formatter: ConfigFormatter.Info.zod.optional(), - lsp: ConfigLSP.Info.zod.optional(), - instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"), - layout: Layout.optional().describe("@deprecated Always uses stretch layout."), - permission: ConfigPermission.Info.optional(), - tools: z.record(z.string(), z.boolean()).optional(), - enterprise: z - .object({ - url: z.string().optional().describe("Enterprise URL"), - }) - .optional(), - compaction: z - .object({ - auto: z.boolean().optional().describe("Enable automatic compaction when context is full (default: true)"), - prune: z.boolean().optional().describe("Enable pruning of old tool outputs (default: true)"), - reserved: z - .number() - .int() - .min(0) - .optional() - .describe("Token buffer for compaction. Leaves enough window to avoid overflow during compaction."), - }) - .optional(), - experimental: z - .object({ - disable_paste_summary: z.boolean().optional(), - batch_tool: z.boolean().optional().describe("Enable the batch tool"), - openTelemetry: z - .boolean() - .optional() - .describe("Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)"), - primary_tools: z - .array(z.string()) - .optional() - .describe("Tools that should only be available to primary agents."), - continue_loop_on_deny: z.boolean().optional().describe("Continue the agent loop when a tool call is denied"), - mcp_timeout: z - .number() - .int() - .positive() - .optional() - .describe("Timeout in milliseconds for model context protocol (MCP) requests"), - }) - .optional(), - }) + title: Schema.optional(AgentRef), + summary: Schema.optional(AgentRef), + compaction: Schema.optional(AgentRef), + }), + [Schema.Record(Schema.String, AgentRef)], + ), + ).annotate({ description: "Agent configuration, see https://opencode.ai/docs/agents" }), + provider: Schema.optional(Schema.Record(Schema.String, ConfigProvider.Info)).annotate({ + description: "Custom provider configurations and model overrides", + }), + mcp: Schema.optional( + Schema.Record( + Schema.String, + Schema.Union([ + ConfigMCP.Info, + // Matches the legacy `{ enabled: false }` form used to disable a server. + Schema.Any.annotate({ [ZodOverride]: z.object({ enabled: z.boolean() }).strict() }), + ]), + ), + ).annotate({ description: "MCP (Model Context Protocol) server configurations" }), + formatter: Schema.optional(ConfigFormatter.Info), + lsp: Schema.optional(ConfigLSP.Info), + instructions: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({ + description: "Additional instruction files or patterns to include", + }), + layout: Schema.optional(ConfigLayout.Layout).annotate({ description: "@deprecated Always uses stretch layout." }), + permission: Schema.optional(PermissionRef), + tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)), + enterprise: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String).annotate({ description: "Enterprise URL" }), + }), + ), + compaction: Schema.optional( + Schema.Struct({ + auto: Schema.optional(Schema.Boolean).annotate({ + description: "Enable automatic compaction when context is full (default: true)", + }), + prune: Schema.optional(Schema.Boolean).annotate({ + description: "Enable pruning of old tool outputs (default: true)", + }), + reserved: Schema.optional(NonNegativeInt).annotate({ + description: "Token buffer for compaction. Leaves enough window to avoid overflow during compaction.", + }), + }), + ), + experimental: Schema.optional( + Schema.Struct({ + disable_paste_summary: Schema.optional(Schema.Boolean), + batch_tool: Schema.optional(Schema.Boolean).annotate({ description: "Enable the batch tool" }), + openTelemetry: Schema.optional(Schema.Boolean).annotate({ + description: "Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)", + }), + primary_tools: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({ + description: "Tools that should only be available to primary agents.", + }), + continue_loop_on_deny: Schema.optional(Schema.Boolean).annotate({ + description: "Continue the agent loop when a tool call is denied", + }), + mcp_timeout: Schema.optional(PositiveInt).annotate({ + description: "Timeout in milliseconds for model context protocol (MCP) requests", + }), + }), + ), +}) + +// Schema.Struct produces readonly types by default, but the service code +// below mutates Info objects directly (e.g. `config.mode = ...`). Strip the +// readonly recursively so callers get the same mutable shape zod inferred. +// +// `Types.DeepMutable` from effect-smol would be a drop-in, but its fallback +// branch `{ -readonly [K in keyof T]: ... }` collapses `unknown` to `{}` +// (since `keyof unknown = never`), which widens `Record` +// fields like `ConfigPlugin.Options`. The local version gates on +// `extends object` so `unknown` passes through. +// +// Tuple branch preserves `ConfigPlugin.Spec`'s `readonly [string, Options]` +// shape (otherwise the general array branch widens it to an array). +type DeepMutable = T extends readonly [unknown, ...unknown[]] + ? { -readonly [K in keyof T]: DeepMutable } + : T extends readonly (infer U)[] + ? DeepMutable[] + : T extends object + ? { -readonly [K in keyof T]: DeepMutable } + : T + +// The walker emits `z.object({...})` which is non-strict by default. Config +// historically uses `.strict()` (additionalProperties: false in openapi.json), +// so layer that on after derivation. Re-apply the Config ref afterward +// since `.strict()` strips the walker's meta annotation. +export const Info = (zod(InfoSchema) as unknown as z.ZodObject) .strict() - .meta({ - ref: "Config", - }) + .meta({ ref: "Config" }) as unknown as z.ZodType>> export type Info = z.output & { // plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together From b382d1a4677622f94c9927ef3ccbd38e74bd5799 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 23:51:30 -0400 Subject: [PATCH 180/335] docs(effect): track schema migration progress with concrete file checklists (#23242) --- packages/opencode/specs/effect/schema.md | 278 ++++++++++++++++++++--- 1 file changed, 252 insertions(+), 26 deletions(-) diff --git a/packages/opencode/specs/effect/schema.md b/packages/opencode/specs/effect/schema.md index eed69e52b7..2cf80b98ee 100644 --- a/packages/opencode/specs/effect/schema.md +++ b/packages/opencode/specs/effect/schema.md @@ -1,12 +1,19 @@ # Schema migration -Practical reference for migrating data types in `packages/opencode` from Zod-first definitions to Effect Schema with Zod compatibility shims. +Practical reference for migrating data types in `packages/opencode` from +Zod-first definitions to Effect Schema with Zod compatibility shims. ## Goal -Use Effect Schema as the source of truth for domain models, IDs, inputs, outputs, and typed errors. +Use Effect Schema as the source of truth for domain models, IDs, inputs, +outputs, and typed errors. Keep Zod available at existing HTTP, tool, and +compatibility boundaries by exposing a `.zod` static derived from the Effect +schema via `@/util/effect-zod`. -Keep Zod available at existing HTTP, tool, and compatibility boundaries by exposing a `.zod` field derived from the Effect schema. +The long-term driver is `specs/effect/http-api.md` — once the HTTP server +moves to `@effect/platform`, every Schema-first DTO can flow through +`HttpApi` / `HttpRouter` without a zod translation layer, and the entire +`effect-zod` walker plus every `.zod` static can be deleted. ## Preferred shapes @@ -24,17 +31,14 @@ export class Info extends Schema.Class("Foo.Info")({ } ``` -If the class cannot reference itself cleanly during initialization, use the existing two-step pattern: +If the class cannot reference itself cleanly during initialization, use the +two-step `withStatics` pattern: ```ts -const _Info = Schema.Struct({ +export const Info = Schema.Struct({ id: FooID, name: Schema.String, -}) - -export const Info = Object.assign(_Info, { - zod: zod(_Info), -}) +}).pipe(withStatics((s) => ({ zod: zod(s) }))) ``` ### Errors @@ -49,27 +53,89 @@ export class NotFoundError extends Schema.TaggedErrorClass()("Foo ### IDs and branded leaf types -Keep branded/schema-backed IDs as Effect schemas and expose `static readonly zod` for compatibility when callers still expect Zod. +Keep branded/schema-backed IDs as Effect schemas and expose +`static readonly zod` for compatibility when callers still expect Zod. + +### Refinements + +Reuse named refinements instead of re-spelling `z.number().int().positive()` +in every schema. The `effect-zod` walker translates the Effect versions into +the corresponding zod methods, so JSON Schema output (`type: integer`, +`exclusiveMinimum`, `pattern`, `format: uuid`, …) is preserved. + +```ts +const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)) +const NonNegativeInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) +const HexColor = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)) +``` + +See `test/util/effect-zod.test.ts` for the full set of translated checks. ## Compatibility rule -During migration, route validators, tool parameters, and any existing Zod-based boundary should consume the derived `.zod` schema instead of maintaining a second hand-written Zod schema. +During migration, route validators, tool parameters, and any existing +Zod-based boundary should consume the derived `.zod` schema instead of +maintaining a second hand-written Zod schema. The default should be: - Effect Schema owns the type - `.zod` exists only as a compatibility surface -- new domain models should not start Zod-first unless there is a concrete boundary-specific need +- new domain models should not start Zod-first unless there is a concrete + boundary-specific need ## When Zod can stay It is fine to keep a Zod-native schema temporarily when: -- the type is only used at an HTTP or tool boundary +- the type is only used at an HTTP or tool boundary and is not reused elsewhere - the validator depends on Zod-only transforms or behavior not yet covered by `zod()` - the migration would force unrelated churn across a large call graph -When this happens, prefer leaving a short note or TODO rather than silently creating a parallel schema source of truth. +When this happens, prefer leaving a short note or TODO rather than silently +creating a parallel schema source of truth. + +## Escape hatches + +The walker in `@/util/effect-zod` exposes three explicit escape hatches for +cases the pure-Schema path cannot express. Each one stays in the codebase +only as long as its upstream or local dependency requires it — inline +comments document when each can be deleted. + +### `ZodOverride` annotation + +Replaces the entire derivation with a hand-crafted zod schema. Used when: + +- the target carries external `$ref` metadata (e.g. + `config/model-id.ts` points at `https://models.dev/...`) +- the target is a zod-only schema that cannot yet be expressed as Schema + (e.g. `ConfigAgent.Info`, `ConfigPermission.Info`, `Log.Level`) + +### `ZodPreprocess` annotation + +Wraps the derived zod schema with `z.preprocess(fn, inner)`. Used by +`config/permission.ts` to inject `__originalKeys` before parsing, because +`Schema.StructWithRest` canonicalises output (known fields first, catchall +after) and destroys the user's original property order — which permission +rule precedence depends on. + +Tracked upstream as `effect:core/wlh553`: "Schema: add preserveInputOrder +(or pre-parse hook) for open structs." Once that lands, `ZodPreprocess` and +the `__originalKeys` hack can both be deleted. + +### Local `DeepMutable` in `config/config.ts` + +`Schema.Struct` produces `readonly` types. Some consumer code (notably the +`Config` service) mutates `Info` objects directly, so a readonly-stripping +utility is needed when casting the derived zod schema's output type. + +`Types.DeepMutable` from effect-smol would be a drop-in, but it widens +`unknown` to `{}` in the fallback branch — a bug that affects any schema +using `Schema.Record(String, Schema.Unknown)`. + +Tracked upstream as `effect:core/x228my`: "Types.DeepMutable widens unknown +to `{}`." Once that lands, the local `DeepMutable` copy can be deleted and +`Types.DeepMutable` used directly. ## Ordering @@ -81,19 +147,179 @@ Migrate in this order: 4. Service-local internal models 5. Route and tool boundary validators that can switch to `.zod` -This keeps shared types canonical first and makes boundary updates mostly mechanical. +This keeps shared types canonical first and makes boundary updates mostly +mechanical. -## Checklist +## Progress tracker -- [ ] Shared `schema.ts` leaf models are Effect Schema-first -- [ ] Exported `Info` / `Input` / `Output` types use `Schema.Class` where appropriate -- [ ] Domain errors use `Schema.TaggedErrorClass` -- [ ] Migrated types expose `.zod` for back compatibility -- [ ] Route and tool validators consume derived `.zod` instead of duplicate Zod definitions -- [ ] New domain models default to Effect Schema first +### `src/config/` ✅ complete + +All of `packages/opencode/src/config/` has been migrated. Files that still +import `z` do so only for local `ZodOverride` bridges or for `z.ZodType` +type annotations — the `export const ` values are all Effect +Schema at source. + +- [x] skills, formatter, console-state, mcp, lsp, permission (leaves), model-id, command, plugin, provider +- [x] server, layout +- [x] keybinds +- [x] permission#Info +- [x] agent +- [x] config.ts root + +### `src/*/schema.ts` leaf modules + +These are the highest-priority next targets. Each is a small, self-contained +schema module with a clear domain. + +- [ ] `src/control-plane/schema.ts` +- [ ] `src/permission/schema.ts` +- [ ] `src/project/schema.ts` +- [ ] `src/provider/schema.ts` +- [ ] `src/pty/schema.ts` +- [ ] `src/question/schema.ts` +- [ ] `src/session/schema.ts` +- [ ] `src/sync/schema.ts` +- [ ] `src/tool/schema.ts` + +### Session domain + +Major cluster. Message + event types flow through the SSE API and every SDK +output, so byte-identical SDK surface is critical. + +- [ ] `src/session/compaction.ts` +- [ ] `src/session/message-v2.ts` +- [ ] `src/session/message.ts` +- [ ] `src/session/prompt.ts` +- [ ] `src/session/revert.ts` +- [ ] `src/session/session.ts` +- [ ] `src/session/status.ts` +- [ ] `src/session/summary.ts` +- [ ] `src/session/todo.ts` + +### Provider domain + +- [ ] `src/provider/auth.ts` +- [ ] `src/provider/models.ts` +- [ ] `src/provider/provider.ts` + +### Tool schemas + +Each tool declares its parameters via a zod schema. Tools are consumed by +both the in-process runtime and the AI SDK's tool-calling layer, so the +emitted JSON Schema must stay byte-identical. + +- [ ] `src/tool/apply_patch.ts` +- [ ] `src/tool/bash.ts` +- [ ] `src/tool/codesearch.ts` +- [ ] `src/tool/edit.ts` +- [ ] `src/tool/glob.ts` +- [ ] `src/tool/grep.ts` +- [ ] `src/tool/invalid.ts` +- [ ] `src/tool/lsp.ts` +- [ ] `src/tool/multiedit.ts` +- [ ] `src/tool/plan.ts` +- [ ] `src/tool/question.ts` +- [ ] `src/tool/read.ts` +- [ ] `src/tool/registry.ts` +- [ ] `src/tool/skill.ts` +- [ ] `src/tool/task.ts` +- [ ] `src/tool/todo.ts` +- [ ] `src/tool/tool.ts` +- [ ] `src/tool/webfetch.ts` +- [ ] `src/tool/websearch.ts` +- [ ] `src/tool/write.ts` + +### HTTP route boundaries + +Every file in `src/server/routes/` uses hono-openapi with zod validators for +route inputs/outputs. Migrating these individually is the last step; most +will switch to `.zod` derived from the Schema-migrated domain types above, +which means touching them is largely mechanical once the domain side is +done. + +- [ ] `src/server/error.ts` +- [ ] `src/server/event.ts` +- [ ] `src/server/projectors.ts` +- [ ] `src/server/routes/control/index.ts` +- [ ] `src/server/routes/control/workspace.ts` +- [ ] `src/server/routes/global.ts` +- [ ] `src/server/routes/instance/index.ts` +- [ ] `src/server/routes/instance/config.ts` +- [ ] `src/server/routes/instance/event.ts` +- [ ] `src/server/routes/instance/experimental.ts` +- [ ] `src/server/routes/instance/file.ts` +- [ ] `src/server/routes/instance/mcp.ts` +- [ ] `src/server/routes/instance/permission.ts` +- [ ] `src/server/routes/instance/project.ts` +- [ ] `src/server/routes/instance/provider.ts` +- [ ] `src/server/routes/instance/pty.ts` +- [ ] `src/server/routes/instance/question.ts` +- [ ] `src/server/routes/instance/session.ts` +- [ ] `src/server/routes/instance/sync.ts` +- [ ] `src/server/routes/instance/tui.ts` + +The bigger prize for this group is the `@effect/platform` HTTP migration +described in `specs/effect/http-api.md`. Once that lands, every one of +these files changes shape entirely (`HttpApi.endpoint(...)` and friends), +so the Schema-first domain types become a prerequisite rather than a +sibling task. + +### Everything else + +Small / shared / control-plane / CLI. Mostly independent; can be done +piecewise. + +- [ ] `src/acp/agent.ts` +- [ ] `src/agent/agent.ts` +- [ ] `src/bus/bus-event.ts` +- [ ] `src/bus/index.ts` +- [ ] `src/cli/cmd/tui/config/tui-migrate.ts` +- [ ] `src/cli/cmd/tui/config/tui-schema.ts` +- [ ] `src/cli/cmd/tui/config/tui.ts` +- [ ] `src/cli/cmd/tui/event.ts` +- [ ] `src/cli/ui.ts` +- [ ] `src/command/index.ts` +- [ ] `src/control-plane/adaptors/worktree.ts` +- [ ] `src/control-plane/types.ts` +- [ ] `src/control-plane/workspace.ts` +- [ ] `src/file/index.ts` +- [ ] `src/file/ripgrep.ts` +- [ ] `src/file/watcher.ts` +- [ ] `src/format/index.ts` +- [ ] `src/id/id.ts` +- [ ] `src/ide/index.ts` +- [ ] `src/installation/index.ts` +- [ ] `src/lsp/client.ts` +- [ ] `src/lsp/lsp.ts` +- [ ] `src/mcp/auth.ts` +- [ ] `src/patch/index.ts` +- [ ] `src/plugin/github-copilot/models.ts` +- [ ] `src/project/project.ts` +- [ ] `src/project/vcs.ts` +- [ ] `src/pty/index.ts` +- [ ] `src/skill/index.ts` +- [ ] `src/snapshot/index.ts` +- [ ] `src/storage/db.ts` +- [ ] `src/storage/storage.ts` +- [ ] `src/sync/index.ts` +- [ ] `src/util/fn.ts` +- [ ] `src/util/log.ts` +- [ ] `src/util/update-schema.ts` +- [ ] `src/worktree/index.ts` + +### Do-not-migrate + +- `src/util/effect-zod.ts` — the walker itself. Stays zod-importing forever + (it's what emits zod from Schema). Goes away only when the `.zod` + compatibility layer is no longer needed anywhere. ## Notes -- Use `@/util/effect-zod` for all Schema -> Zod conversion. -- Prefer one canonical schema definition. Avoid maintaining parallel Zod and Effect definitions for the same domain type. -- Keep the migration incremental. Converting the domain model first is more valuable than converting every boundary in the same change. +- Use `@/util/effect-zod` for all Schema → Zod conversion. +- Prefer one canonical schema definition. Avoid maintaining parallel Zod and + Effect definitions for the same domain type. +- Keep the migration incremental. Converting the domain model first is more + valuable than converting every boundary in the same change. +- Every migrated file should leave the generated SDK output (`packages/sdk/ + openapi.json` and `packages/sdk/js/src/v2/gen/types.gen.ts`) byte-identical + unless the change is deliberately user-visible. From 5e9d5c734ea883c00bdb2936e3bd6d786b220db4 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 18 Apr 2026 03:52:28 +0000 Subject: [PATCH 181/335] chore: generate --- packages/opencode/specs/effect/schema.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/opencode/specs/effect/schema.md b/packages/opencode/specs/effect/schema.md index 2cf80b98ee..72ee10350d 100644 --- a/packages/opencode/specs/effect/schema.md +++ b/packages/opencode/specs/effect/schema.md @@ -64,9 +64,9 @@ the corresponding zod methods, so JSON Schema output (`type: integer`, `exclusiveMinimum`, `pattern`, `format: uuid`, …) is preserved. ```ts -const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)) +const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0)) const NonNegativeInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) -const HexColor = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)) +const HexColor = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)) ``` See `test/util/effect-zod.test.ts` for the full set of translated checks. @@ -321,5 +321,5 @@ piecewise. - Keep the migration incremental. Converting the domain model first is more valuable than converting every boundary in the same change. - Every migrated file should leave the generated SDK output (`packages/sdk/ - openapi.json` and `packages/sdk/js/src/v2/gen/types.gen.ts`) byte-identical +openapi.json` and `packages/sdk/js/src/v2/gen/types.gen.ts`) byte-identical unless the change is deliberately user-visible. From 9c16bd1e30d631d482ae696f426bf5f7eb73dbdb Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 17 Apr 2026 23:51:16 -0500 Subject: [PATCH 182/335] fix: make skills logic more token efficient (#23253) --- packages/opencode/src/tool/skill.ts | 127 +++++++++------------- packages/opencode/src/tool/skill.txt | 5 + packages/opencode/test/tool/skill.test.ts | 96 ---------------- 3 files changed, 57 insertions(+), 171 deletions(-) create mode 100644 packages/opencode/src/tool/skill.txt diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index 58a66ee744..d86faec2b4 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -3,10 +3,10 @@ import { pathToFileURL } from "url" import z from "zod" import { Effect } from "effect" import * as Stream from "effect/Stream" -import { EffectLogger } from "@/effect" import { Ripgrep } from "../file/ripgrep" import { Skill } from "../skill" import * as Tool from "./tool" +import DESCRIPTION from "./skill.txt" const Parameters = z.object({ name: z.string().describe("The name of the skill from available_skills"), @@ -18,82 +18,59 @@ export const SkillTool = Tool.define( const skill = yield* Skill.Service const rg = yield* Ripgrep.Service - return () => - Effect.gen(function* () { - const list = yield* skill.available().pipe(Effect.provide(EffectLogger.layer)) + return { + description: DESCRIPTION, + parameters: Parameters, + execute: (params: z.infer, ctx: Tool.Context) => + Effect.gen(function* () { + const info = yield* skill.get(params.name) + if (!info) { + const all = yield* skill.all() + const available = all.map((item) => item.name).join(", ") + throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`) + } - const description = - list.length === 0 - ? "Load a specialized skill that provides domain-specific instructions and workflows. No skills are currently available." - : [ - "Load a specialized skill that provides domain-specific instructions and workflows.", - "", - "When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.", - "", - "The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.", - "", - 'Tool output includes a `` block with the loaded content.', - "", - "The following skills provide specialized sets of instructions for particular tasks", - "Invoke this tool to load a skill when a task matches one of the available skills listed below:", - "", - Skill.fmt(list, { verbose: false }), - ].join("\n") + yield* ctx.ask({ + permission: "skill", + patterns: [params.name], + always: [params.name], + metadata: {}, + }) - return { - description, - parameters: Parameters, - execute: (params: z.infer, ctx: Tool.Context) => - Effect.gen(function* () { - const info = yield* skill.get(params.name) - if (!info) { - const all = yield* skill.all() - const available = all.map((item) => item.name).join(", ") - throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`) - } + const dir = path.dirname(info.location) + const base = pathToFileURL(dir).href + const limit = 10 + const files = yield* rg.files({ cwd: dir, follow: false, hidden: true, signal: ctx.abort }).pipe( + Stream.filter((file) => !file.includes("SKILL.md")), + Stream.map((file) => path.resolve(dir, file)), + Stream.take(limit), + Stream.runCollect, + Effect.map((chunk) => [...chunk].map((file) => `${file}`).join("\n")), + ) - yield* ctx.ask({ - permission: "skill", - patterns: [params.name], - always: [params.name], - metadata: {}, - }) - - const dir = path.dirname(info.location) - const base = pathToFileURL(dir).href - const limit = 10 - const files = yield* rg.files({ cwd: dir, follow: false, hidden: true, signal: ctx.abort }).pipe( - Stream.filter((file) => !file.includes("SKILL.md")), - Stream.map((file) => path.resolve(dir, file)), - Stream.take(limit), - Stream.runCollect, - Effect.map((chunk) => [...chunk].map((file) => `${file}`).join("\n")), - ) - - return { - title: `Loaded skill: ${info.name}`, - output: [ - ``, - `# Skill: ${info.name}`, - "", - info.content.trim(), - "", - `Base directory for this skill: ${base}`, - "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.", - "Note: file list is sampled.", - "", - "", - files, - "", - "", - ].join("\n"), - metadata: { - name: info.name, - dir, - }, - } - }).pipe(Effect.orDie), - } - }) + return { + title: `Loaded skill: ${info.name}`, + output: [ + ``, + `# Skill: ${info.name}`, + "", + info.content.trim(), + "", + `Base directory for this skill: ${base}`, + "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.", + "Note: file list is sampled.", + "", + "", + files, + "", + "", + ].join("\n"), + metadata: { + name: info.name, + dir, + }, + } + }).pipe(Effect.orDie), + } }), ) diff --git a/packages/opencode/src/tool/skill.txt b/packages/opencode/src/tool/skill.txt new file mode 100644 index 0000000000..44d990317a --- /dev/null +++ b/packages/opencode/src/tool/skill.txt @@ -0,0 +1,5 @@ +Load a specialized skill when the task at hand matches one of the skills listed in the system prompt. + +Use this tool to inject the skill's instructions and resources into current conversation. The output may contain detailed workflow guidance as well as references to scripts, files, etc in the same directory as the skill. + +The skill name must match one of the skills listed in your system prompt. diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts index 55e126ab47..b12940e4dc 100644 --- a/packages/opencode/test/tool/skill.test.ts +++ b/packages/opencode/test/tool/skill.test.ts @@ -31,102 +31,6 @@ const node = CrossSpawnSpawner.defaultLayer const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node)) describe("tool.skill", () => { - it.live("description lists skill location URL", () => - provideTmpdirInstance( - (dir) => - Effect.gen(function* () { - const skill = path.join(dir, ".opencode", "skill", "tool-skill") - yield* Effect.promise(() => - Bun.write( - path.join(skill, "SKILL.md"), - `--- -name: tool-skill -description: Skill for tool tests. ---- - -# Tool Skill -`, - ), - ) - const home = process.env.OPENCODE_TEST_HOME - process.env.OPENCODE_TEST_HOME = dir - yield* Effect.addFinalizer(() => - Effect.sync(() => { - process.env.OPENCODE_TEST_HOME = home - }), - ) - const registry = yield* ToolRegistry.Service - const desc = - (yield* registry.tools({ - providerID: "opencode" as any, - modelID: "gpt-5" as any, - agent: { name: "build", mode: "primary", permission: [], options: {} }, - })).find((tool) => tool.id === SkillTool.id)?.description ?? "" - expect(desc).toContain("**tool-skill**: Skill for tool tests.") - }), - { git: true }, - ), - ) - - it.live("description sorts skills by name and is stable across calls", () => - provideTmpdirInstance( - (dir) => - Effect.gen(function* () { - for (const [name, description] of [ - ["zeta-skill", "Zeta skill."], - ["alpha-skill", "Alpha skill."], - ["middle-skill", "Middle skill."], - ]) { - const skill = path.join(dir, ".opencode", "skill", name) - yield* Effect.promise(() => - Bun.write( - path.join(skill, "SKILL.md"), - `--- -name: ${name} -description: ${description} ---- - -# ${name} -`, - ), - ) - } - const home = process.env.OPENCODE_TEST_HOME - process.env.OPENCODE_TEST_HOME = dir - yield* Effect.addFinalizer(() => - Effect.sync(() => { - process.env.OPENCODE_TEST_HOME = home - }), - ) - - const agent = { name: "build", mode: "primary" as const, permission: [], options: {} } - const registry = yield* ToolRegistry.Service - const load = Effect.fnUntraced(function* () { - return ( - (yield* registry.tools({ - providerID: "opencode" as any, - modelID: "gpt-5" as any, - agent, - })).find((tool) => tool.id === SkillTool.id)?.description ?? "" - ) - }) - const first = yield* load() - const second = yield* load() - - expect(first).toBe(second) - - const alpha = first.indexOf("**alpha-skill**: Alpha skill.") - const middle = first.indexOf("**middle-skill**: Middle skill.") - const zeta = first.indexOf("**zeta-skill**: Zeta skill.") - - expect(alpha).toBeGreaterThan(-1) - expect(middle).toBeGreaterThan(alpha) - expect(zeta).toBeGreaterThan(middle) - }), - { git: true }, - ), - ) - it.live("execute returns skill content block with files", () => provideTmpdirInstance( (dir) => From 11cd4fb63904768da73fad323dc82a83e052f87c Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 18 Apr 2026 01:46:26 -0400 Subject: [PATCH 183/335] core: extract session entry stepping logic into dedicated module Move the step function from session-entry.ts to session-entry-stepper.ts and remove immer dependency. Add static fromEvent factory methods to Synthetic, Assistant, and Compaction classes for cleaner event-to-entry conversion. --- .../opencode/src/v2/session-entry-stepper.ts | 253 ++++++++++++++++++ packages/opencode/src/v2/session-entry.ts | 178 ++---------- ....test.ts => session-entry-stepper.test.ts} | 188 +++++++++++-- 3 files changed, 446 insertions(+), 173 deletions(-) create mode 100644 packages/opencode/src/v2/session-entry-stepper.ts rename packages/opencode/test/session/{session-entry.test.ts => session-entry-stepper.test.ts} (76%) diff --git a/packages/opencode/src/v2/session-entry-stepper.ts b/packages/opencode/src/v2/session-entry-stepper.ts new file mode 100644 index 0000000000..3d642579d0 --- /dev/null +++ b/packages/opencode/src/v2/session-entry-stepper.ts @@ -0,0 +1,253 @@ +import { castDraft, produce, type WritableDraft } from "immer" +import { SessionEvent } from "./session-event" +import { SessionEntry } from "./session-entry" + +export type MemoryState = { + entries: SessionEntry.Entry[] + pending: SessionEntry.Entry[] +} + +export interface Adapter { + readonly getCurrentAssistant: () => SessionEntry.Assistant | undefined + readonly updateAssistant: (assistant: SessionEntry.Assistant) => void + readonly appendEntry: (entry: SessionEntry.Entry) => void + readonly appendPending: (entry: SessionEntry.Entry) => void + readonly finish: () => Result +} + +export function memory(state: MemoryState): Adapter { + const activeAssistantIndex = () => + state.entries.findLastIndex((entry) => entry.type === "assistant" && !entry.time.completed) + + return { + getCurrentAssistant() { + const index = activeAssistantIndex() + if (index < 0) return + const assistant = state.entries[index] + return assistant?.type === "assistant" ? assistant : undefined + }, + updateAssistant(assistant) { + const index = activeAssistantIndex() + if (index < 0) return + const current = state.entries[index] + if (current?.type !== "assistant") return + state.entries[index] = assistant + }, + appendEntry(entry) { + state.entries.push(entry) + }, + appendPending(entry) { + state.pending.push(entry) + }, + finish() { + return state + }, + } +} + +export function stepWith(adapter: Adapter, event: SessionEvent.Event): Result { + const currentAssistant = adapter.getCurrentAssistant() + type DraftAssistant = WritableDraft + type DraftTool = WritableDraft + type DraftText = WritableDraft + type DraftReasoning = WritableDraft + + const latestTool = (assistant: DraftAssistant | undefined, callID?: string) => + assistant?.content.findLast( + (item): item is DraftTool => item.type === "tool" && (callID === undefined || item.callID === callID), + ) + + const latestText = (assistant: DraftAssistant | undefined) => + assistant?.content.findLast((item): item is DraftText => item.type === "text") + + const latestReasoning = (assistant: DraftAssistant | undefined) => + assistant?.content.findLast((item): item is DraftReasoning => item.type === "reasoning") + + SessionEvent.Event.match(event, { + prompt: (event) => { + const entry = SessionEntry.User.fromEvent(event) + if (currentAssistant) { + adapter.appendPending(entry) + return + } + adapter.appendEntry(entry) + }, + synthetic: (event) => { + adapter.appendEntry(SessionEntry.Synthetic.fromEvent(event)) + }, + "step.started": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + draft.time.completed = event.timestamp + }), + ) + } + adapter.appendEntry(SessionEntry.Assistant.fromEvent(event)) + }, + "step.ended": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + draft.time.completed = event.timestamp + draft.cost = event.cost + draft.tokens = event.tokens + }), + ) + } + }, + "text.started": () => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + draft.content.push({ + type: "text", + text: "", + }) + }), + ) + } + }, + "text.delta": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + const match = latestText(draft) + if (match) match.text += event.delta + }), + ) + } + }, + "text.ended": () => {}, + "tool.input.started": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + draft.content.push({ + type: "tool", + callID: event.callID, + name: event.name, + time: { + created: event.timestamp, + }, + state: { + status: "pending", + input: "", + }, + }) + }), + ) + } + }, + "tool.input.delta": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + const match = latestTool(draft, event.callID) + // oxlint-disable-next-line no-base-to-string -- event.delta is a Schema.String (runtime string) + if (match && match.state.status === "pending") match.state.input += event.delta + }), + ) + } + }, + "tool.input.ended": () => {}, + "tool.called": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + const match = latestTool(draft, event.callID) + if (match) { + match.time.ran = event.timestamp + match.state = { + status: "running", + input: event.input, + } + } + }), + ) + } + }, + "tool.success": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + const match = latestTool(draft, event.callID) + if (match && match.state.status === "running") { + match.state = { + status: "completed", + input: match.state.input, + output: event.output ?? "", + title: event.title, + metadata: event.metadata ?? {}, + attachments: [...(event.attachments ?? [])], + } + } + }), + ) + } + }, + "tool.error": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + const match = latestTool(draft, event.callID) + if (match && match.state.status === "running") { + match.state = { + status: "error", + error: event.error, + input: match.state.input, + metadata: event.metadata ?? {}, + } + } + }), + ) + } + }, + "reasoning.started": () => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + draft.content.push({ + type: "reasoning", + text: "", + }) + }), + ) + } + }, + "reasoning.delta": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + const match = latestReasoning(draft) + if (match) match.text += event.delta + }), + ) + } + }, + "reasoning.ended": (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + const match = latestReasoning(draft) + if (match) match.text = event.text + }), + ) + } + }, + retried: () => {}, + compacted: (event) => { + adapter.appendEntry(SessionEntry.Compaction.fromEvent(event)) + }, + }) + + return adapter.finish() +} + +export function step(old: MemoryState, event: SessionEvent.Event): MemoryState { + return produce(old, (draft) => { + stepWith(memory(draft as MemoryState), event) + }) +} + +export * as SessionEntryStepper from "./session-entry-stepper" diff --git a/packages/opencode/src/v2/session-entry.ts b/packages/opencode/src/v2/session-entry.ts index 08122428ae..97c5fc7ce9 100644 --- a/packages/opencode/src/v2/session-entry.ts +++ b/packages/opencode/src/v2/session-entry.ts @@ -1,6 +1,5 @@ import { Schema } from "effect" import { SessionEvent } from "./session-event" -import { castDraft, produce } from "immer" export const ID = SessionEvent.ID export type ID = Schema.Schema.Type @@ -40,7 +39,14 @@ export class Synthetic extends Schema.Class("Session.Entry.Synthetic" ...SessionEvent.Synthetic.fields, ...Base, type: Schema.Literal("synthetic"), -}) {} +}) { + static fromEvent(event: SessionEvent.Synthetic) { + return new Synthetic({ + ...event, + time: { created: event.timestamp }, + }) + } +} export class ToolStatePending extends Schema.Class("Session.Entry.ToolState.Pending")({ status: Schema.Literal("pending"), @@ -122,13 +128,32 @@ export class Assistant extends Schema.Class("Session.Entry.Assistant" created: Schema.DateTimeUtc, completed: Schema.DateTimeUtc.pipe(Schema.optional), }), -}) {} +}) { + static fromEvent(event: SessionEvent.Step.Started) { + return new Assistant({ + id: event.id, + type: "assistant", + time: { + created: event.timestamp, + }, + content: [], + }) + } +} export class Compaction extends Schema.Class("Session.Entry.Compaction")({ ...SessionEvent.Compacted.fields, type: Schema.Literal("compaction"), ...Base, -}) {} +}) { + static fromEvent(event: SessionEvent.Compacted) { + return new Compaction({ + ...event, + type: "compaction", + time: { created: event.timestamp }, + }) + } +} export const Entry = Schema.Union([User, Synthetic, Assistant, Compaction]).pipe(Schema.toTaggedUnion("type")) @@ -136,151 +161,6 @@ export type Entry = Schema.Schema.Type export type Type = Entry["type"] -export type History = { - entries: Entry[] - pending: Entry[] -} - -export function step(old: History, event: SessionEvent.Event): History { - return produce(old, (draft) => { - const lastAssistant = draft.entries.findLast((x) => x.type === "assistant") - const pendingAssistant = lastAssistant && !lastAssistant.time.completed ? lastAssistant : undefined - type DraftContent = NonNullable["content"][number] - type DraftTool = Extract - - const latestTool = (callID?: string) => - pendingAssistant?.content.findLast( - (item): item is DraftTool => item.type === "tool" && (callID === undefined || item.callID === callID), - ) - const latestText = () => pendingAssistant?.content.findLast((item) => item.type === "text") - const latestReasoning = () => pendingAssistant?.content.findLast((item) => item.type === "reasoning") - - SessionEvent.Event.match(event, { - prompt: (event) => { - const entry = User.fromEvent(event) - if (pendingAssistant) { - draft.pending.push(castDraft(entry)) - return - } - draft.entries.push(castDraft(entry)) - }, - synthetic: (event) => { - draft.entries.push(new Synthetic({ ...event, time: { created: event.timestamp } })) - }, - "step.started": (event) => { - if (pendingAssistant) pendingAssistant.time.completed = event.timestamp - draft.entries.push({ - id: event.id, - type: "assistant", - time: { - created: event.timestamp, - }, - content: [], - }) - }, - "step.ended": (event) => { - if (!pendingAssistant) return - pendingAssistant.time.completed = event.timestamp - pendingAssistant.cost = event.cost - pendingAssistant.tokens = event.tokens - }, - "text.started": () => { - if (!pendingAssistant) return - pendingAssistant.content.push({ - type: "text", - text: "", - }) - }, - "text.delta": (event) => { - if (!pendingAssistant) return - const match = latestText() - if (match) match.text += event.delta - }, - "text.ended": () => {}, - "tool.input.started": (event) => { - if (!pendingAssistant) return - pendingAssistant.content.push({ - type: "tool", - callID: event.callID, - name: event.name, - time: { - created: event.timestamp, - }, - state: { - status: "pending", - input: "", - }, - }) - }, - "tool.input.delta": (event) => { - if (!pendingAssistant) return - const match = latestTool(event.callID) - // oxlint-disable-next-line no-base-to-string -- event.delta is a Schema.String (runtime string) - if (match) match.state.input += event.delta - }, - "tool.input.ended": () => {}, - "tool.called": (event) => { - if (!pendingAssistant) return - const match = latestTool(event.callID) - if (match) { - match.time.ran = event.timestamp - match.state = { - status: "running", - input: event.input, - } - } - }, - "tool.success": (event) => { - if (!pendingAssistant) return - const match = latestTool(event.callID) - if (match && match.state.status === "running") { - match.state = { - status: "completed", - input: match.state.input, - output: event.output ?? "", - title: event.title, - metadata: event.metadata ?? {}, - attachments: [...(event.attachments ?? [])], - } - } - }, - "tool.error": (event) => { - if (!pendingAssistant) return - const match = latestTool(event.callID) - if (match && match.state.status === "running") { - match.state = { - status: "error", - error: event.error, - input: match.state.input, - metadata: event.metadata ?? {}, - } - } - }, - "reasoning.started": () => { - if (!pendingAssistant) return - pendingAssistant.content.push({ - type: "reasoning", - text: "", - }) - }, - "reasoning.delta": (event) => { - if (!pendingAssistant) return - const match = latestReasoning() - if (match) match.text += event.delta - }, - "reasoning.ended": (event) => { - if (!pendingAssistant) return - const match = latestReasoning() - if (match) match.text = event.text - }, - retried: () => {}, - compacted: (event) => { - draft.entries.push(new Compaction({ ...event, type: "compaction", time: { created: event.timestamp } })) - }, - }) - }) -} - /* export interface Interface { readonly decode: (row: typeof SessionEntryTable.$inferSelect) => Entry diff --git a/packages/opencode/test/session/session-entry.test.ts b/packages/opencode/test/session/session-entry-stepper.test.ts similarity index 76% rename from packages/opencode/test/session/session-entry.test.ts rename to packages/opencode/test/session/session-entry-stepper.test.ts index dea8da20a0..a81b4c2be4 100644 --- a/packages/opencode/test/session/session-entry.test.ts +++ b/packages/opencode/test/session/session-entry-stepper.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test" import * as DateTime from "effect/DateTime" import * as FastCheck from "effect/testing/FastCheck" import { SessionEntry } from "../../src/v2/session-entry" +import { SessionEntryStepper } from "../../src/v2/session-entry-stepper" import { SessionEvent } from "../../src/v2/session-event" const time = (n: number) => DateTime.makeUnsafe(n) @@ -29,8 +30,8 @@ function assistant() { }) } -function history() { - const state: SessionEntry.History = { +function memoryState() { + const state: SessionEntryStepper.MemoryState = { entries: [], pending: [], } @@ -38,51 +39,189 @@ function history() { } function active() { - const state: SessionEntry.History = { + const state: SessionEntryStepper.MemoryState = { entries: [assistant()], pending: [], } return state } -function run(events: SessionEvent.Event[], state = history()) { - return events.reduce((state, event) => SessionEntry.step(state, event), state) +function run(events: SessionEvent.Event[], state = memoryState()) { + return events.reduce((state, event) => SessionEntryStepper.step(state, event), state) } -function last(state: SessionEntry.History) { +function last(state: SessionEntryStepper.MemoryState) { const entry = [...state.pending, ...state.entries].reverse().find((x) => x.type === "assistant") expect(entry?.type).toBe("assistant") return entry?.type === "assistant" ? entry : undefined } -function texts_of(state: SessionEntry.History) { +function texts_of(state: SessionEntryStepper.MemoryState) { const entry = last(state) if (!entry) return [] return entry.content.filter((x): x is SessionEntry.AssistantText => x.type === "text") } -function reasons(state: SessionEntry.History) { +function reasons(state: SessionEntryStepper.MemoryState) { const entry = last(state) if (!entry) return [] return entry.content.filter((x): x is SessionEntry.AssistantReasoning => x.type === "reasoning") } -function tools(state: SessionEntry.History) { +function tools(state: SessionEntryStepper.MemoryState) { const entry = last(state) if (!entry) return [] return entry.content.filter((x): x is SessionEntry.AssistantTool => x.type === "tool") } -function tool(state: SessionEntry.History, callID: string) { +function tool(state: SessionEntryStepper.MemoryState, callID: string) { return tools(state).find((x) => x.callID === callID) } -describe("session-entry step", () => { - describe("seeded pending assistant", () => { +function adapterStore() { + return { + committed: [] as SessionEntry.Entry[], + deferred: [] as SessionEntry.Entry[], + } +} + +function adapterFor(store: ReturnType): SessionEntryStepper.Adapter { + const activeAssistantIndex = () => + store.committed.findLastIndex((entry) => entry.type === "assistant" && !entry.time.completed) + + const getCurrentAssistant = () => { + const index = activeAssistantIndex() + if (index < 0) return + const assistant = store.committed[index] + return assistant?.type === "assistant" ? assistant : undefined + } + + return { + getCurrentAssistant, + updateAssistant(assistant) { + const index = activeAssistantIndex() + if (index < 0) return + const current = store.committed[index] + if (current?.type !== "assistant") return + store.committed[index] = assistant + }, + appendEntry(entry) { + store.committed.push(entry) + }, + appendPending(entry) { + store.deferred.push(entry) + }, + finish() { + return store + }, + } +} + +describe("session-entry-stepper", () => { + describe("stepWith", () => { + test("reduces through a custom adapter", () => { + const store = adapterStore() + store.committed.push(assistant()) + + SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Prompt.create({ text: "hello", timestamp: time(1) })) + SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Reasoning.Started.create({ timestamp: time(2) })) + SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Reasoning.Delta.create({ delta: "thinking", timestamp: time(3) })) + SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Reasoning.Ended.create({ text: "thought", timestamp: time(4) })) + SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Text.Started.create({ timestamp: time(5) })) + SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Text.Delta.create({ delta: "world", timestamp: time(6) })) + SessionEntryStepper.stepWith( + adapterFor(store), + SessionEvent.Step.Ended.create({ + reason: "stop", + cost: 1, + tokens: { + input: 1, + output: 2, + reasoning: 3, + cache: { + read: 4, + write: 5, + }, + }, + timestamp: time(7), + }), + ) + + expect(store.deferred).toHaveLength(1) + expect(store.deferred[0]?.type).toBe("user") + expect(store.committed).toHaveLength(1) + expect(store.committed[0]?.type).toBe("assistant") + if (store.committed[0]?.type !== "assistant") return + + expect(store.committed[0].content).toEqual([ + { type: "reasoning", text: "thought" }, + { type: "text", text: "world" }, + ]) + expect(store.committed[0].time.completed).toEqual(time(7)) + }) + }) + + describe("memory", () => { + test("tracks and replaces the current assistant", () => { + const state = active() + const adapter = SessionEntryStepper.memory(state) + const current = adapter.getCurrentAssistant() + + expect(current?.type).toBe("assistant") + if (!current) return + + adapter.updateAssistant( + new SessionEntry.Assistant({ + ...current, + content: [new SessionEntry.AssistantText({ type: "text", text: "done" })], + time: { + ...current.time, + completed: time(1), + }, + }), + ) + + expect(adapter.getCurrentAssistant()).toBeUndefined() + expect(state.entries[0]?.type).toBe("assistant") + if (state.entries[0]?.type !== "assistant") return + + expect(state.entries[0].content).toEqual([{ type: "text", text: "done" }]) + expect(state.entries[0].time.completed).toEqual(time(1)) + }) + + test("appends committed and pending entries", () => { + const state = memoryState() + const adapter = SessionEntryStepper.memory(state) + const committed = SessionEntry.User.fromEvent(SessionEvent.Prompt.create({ text: "committed", timestamp: time(1) })) + const pending = SessionEntry.User.fromEvent(SessionEvent.Prompt.create({ text: "pending", timestamp: time(2) })) + + adapter.appendEntry(committed) + adapter.appendPending(pending) + + expect(state.entries).toEqual([committed]) + expect(state.pending).toEqual([pending]) + }) + + test("stepWith through memory records reasoning", () => { + const state = active() + + SessionEntryStepper.stepWith(SessionEntryStepper.memory(state), SessionEvent.Reasoning.Started.create({ timestamp: time(1) })) + SessionEntryStepper.stepWith(SessionEntryStepper.memory(state), SessionEvent.Reasoning.Delta.create({ delta: "draft", timestamp: time(2) })) + SessionEntryStepper.stepWith( + SessionEntryStepper.memory(state), + SessionEvent.Reasoning.Ended.create({ text: "final", timestamp: time(3) }), + ) + + expect(reasons(state)).toEqual([{ type: "reasoning", text: "final" }]) + }) + }) + + describe("step", () => { + describe("seeded pending assistant", () => { test("stores prompts in entries when no assistant is pending", () => { FastCheck.assert( FastCheck.property(word, (body) => { - const next = SessionEntry.step(history(), SessionEvent.Prompt.create({ text: body, timestamp: time(1) })) + const next = SessionEntryStepper.step(memoryState(), SessionEvent.Prompt.create({ text: body, timestamp: time(1) })) expect(next.entries).toHaveLength(1) expect(next.entries[0]?.type).toBe("user") if (next.entries[0]?.type !== "user") return @@ -95,7 +234,7 @@ describe("session-entry step", () => { test("stores prompts in pending when an assistant is pending", () => { FastCheck.assert( FastCheck.property(word, (body) => { - const next = SessionEntry.step(active(), SessionEvent.Prompt.create({ text: body, timestamp: time(1) })) + const next = SessionEntryStepper.step(active(), SessionEvent.Prompt.create({ text: body, timestamp: time(1) })) expect(next.pending).toHaveLength(1) expect(next.pending[0]?.type).toBe("user") if (next.pending[0]?.type !== "user") return @@ -110,8 +249,8 @@ describe("session-entry step", () => { FastCheck.property(texts, (parts) => { const next = parts.reduce( (state, part, i) => - SessionEntry.step(state, SessionEvent.Text.Delta.create({ delta: part, timestamp: time(i + 2) })), - SessionEntry.step(active(), SessionEvent.Text.Started.create({ timestamp: time(1) })), + SessionEntryStepper.step(state, SessionEvent.Text.Delta.create({ delta: part, timestamp: time(i + 2) })), + SessionEntryStepper.step(active(), SessionEvent.Text.Started.create({ timestamp: time(1) })), ) expect(texts_of(next)).toEqual([ @@ -302,7 +441,7 @@ describe("session-entry step", () => { }, timestamp: time(n), }) - const next = SessionEntry.step(active(), event) + const next = SessionEntryStepper.step(active(), event) const entry = last(next) expect(entry).toBeDefined() if (!entry) return @@ -316,12 +455,12 @@ describe("session-entry step", () => { }) }) - describe("known reducer gaps", () => { + describe("known reducer gaps", () => { test("prompt appends immutably when no assistant is pending", () => { FastCheck.assert( FastCheck.property(word, (body) => { - const old = history() - const next = SessionEntry.step(old, SessionEvent.Prompt.create({ text: body, timestamp: time(1) })) + const old = memoryState() + const next = SessionEntryStepper.step(old, SessionEvent.Prompt.create({ text: body, timestamp: time(1) })) expect(old).not.toBe(next) expect(old.entries).toHaveLength(0) expect(next.entries).toHaveLength(1) @@ -334,7 +473,7 @@ describe("session-entry step", () => { FastCheck.assert( FastCheck.property(word, (body) => { const old = active() - const next = SessionEntry.step(old, SessionEvent.Prompt.create({ text: body, timestamp: time(1) })) + const next = SessionEntryStepper.step(old, SessionEvent.Prompt.create({ text: body, timestamp: time(1) })) expect(old).not.toBe(next) expect(old.pending).toHaveLength(0) expect(next.pending).toHaveLength(1) @@ -651,7 +790,7 @@ describe("session-entry step", () => { test("records synthetic events", () => { FastCheck.assert( FastCheck.property(word, (body) => { - const next = SessionEntry.step(history(), SessionEvent.Synthetic.create({ text: body, timestamp: time(1) })) + const next = SessionEntryStepper.step(memoryState(), SessionEvent.Synthetic.create({ text: body, timestamp: time(1) })) expect(next.entries).toHaveLength(1) expect(next.entries[0]?.type).toBe("synthetic") if (next.entries[0]?.type !== "synthetic") return @@ -664,8 +803,8 @@ describe("session-entry step", () => { test("records compaction events", () => { FastCheck.assert( FastCheck.property(FastCheck.boolean(), maybe(FastCheck.boolean()), (auto, overflow) => { - const next = SessionEntry.step( - history(), + const next = SessionEntryStepper.step( + memoryState(), SessionEvent.Compacted.create({ auto, overflow, timestamp: time(1) }), ) expect(next.entries).toHaveLength(1) @@ -677,5 +816,6 @@ describe("session-entry step", () => { { numRuns: 50 }, ) }) + }) }) }) From 95edbc0ae68a5e4f9a24a5d0249391ef18b168f5 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 18 Apr 2026 05:49:37 +0000 Subject: [PATCH 184/335] chore: generate --- .../session/session-entry-stepper.test.ts | 1095 +++++++++-------- 1 file changed, 562 insertions(+), 533 deletions(-) diff --git a/packages/opencode/test/session/session-entry-stepper.test.ts b/packages/opencode/test/session/session-entry-stepper.test.ts index a81b4c2be4..5c7df2dbad 100644 --- a/packages/opencode/test/session/session-entry-stepper.test.ts +++ b/packages/opencode/test/session/session-entry-stepper.test.ts @@ -125,10 +125,19 @@ describe("session-entry-stepper", () => { SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Prompt.create({ text: "hello", timestamp: time(1) })) SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Reasoning.Started.create({ timestamp: time(2) })) - SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Reasoning.Delta.create({ delta: "thinking", timestamp: time(3) })) - SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Reasoning.Ended.create({ text: "thought", timestamp: time(4) })) + SessionEntryStepper.stepWith( + adapterFor(store), + SessionEvent.Reasoning.Delta.create({ delta: "thinking", timestamp: time(3) }), + ) + SessionEntryStepper.stepWith( + adapterFor(store), + SessionEvent.Reasoning.Ended.create({ text: "thought", timestamp: time(4) }), + ) SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Text.Started.create({ timestamp: time(5) })) - SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Text.Delta.create({ delta: "world", timestamp: time(6) })) + SessionEntryStepper.stepWith( + adapterFor(store), + SessionEvent.Text.Delta.create({ delta: "world", timestamp: time(6) }), + ) SessionEntryStepper.stepWith( adapterFor(store), SessionEvent.Step.Ended.create({ @@ -192,7 +201,9 @@ describe("session-entry-stepper", () => { test("appends committed and pending entries", () => { const state = memoryState() const adapter = SessionEntryStepper.memory(state) - const committed = SessionEntry.User.fromEvent(SessionEvent.Prompt.create({ text: "committed", timestamp: time(1) })) + const committed = SessionEntry.User.fromEvent( + SessionEvent.Prompt.create({ text: "committed", timestamp: time(1) }), + ) const pending = SessionEntry.User.fromEvent(SessionEvent.Prompt.create({ text: "pending", timestamp: time(2) })) adapter.appendEntry(committed) @@ -205,8 +216,14 @@ describe("session-entry-stepper", () => { test("stepWith through memory records reasoning", () => { const state = active() - SessionEntryStepper.stepWith(SessionEntryStepper.memory(state), SessionEvent.Reasoning.Started.create({ timestamp: time(1) })) - SessionEntryStepper.stepWith(SessionEntryStepper.memory(state), SessionEvent.Reasoning.Delta.create({ delta: "draft", timestamp: time(2) })) + SessionEntryStepper.stepWith( + SessionEntryStepper.memory(state), + SessionEvent.Reasoning.Started.create({ timestamp: time(1) }), + ) + SessionEntryStepper.stepWith( + SessionEntryStepper.memory(state), + SessionEvent.Reasoning.Delta.create({ delta: "draft", timestamp: time(2) }), + ) SessionEntryStepper.stepWith( SessionEntryStepper.memory(state), SessionEvent.Reasoning.Ended.create({ text: "final", timestamp: time(3) }), @@ -218,377 +235,283 @@ describe("session-entry-stepper", () => { describe("step", () => { describe("seeded pending assistant", () => { - test("stores prompts in entries when no assistant is pending", () => { - FastCheck.assert( - FastCheck.property(word, (body) => { - const next = SessionEntryStepper.step(memoryState(), SessionEvent.Prompt.create({ text: body, timestamp: time(1) })) - expect(next.entries).toHaveLength(1) - expect(next.entries[0]?.type).toBe("user") - if (next.entries[0]?.type !== "user") return - expect(next.entries[0].text).toBe(body) - }), - { numRuns: 50 }, - ) - }) + test("stores prompts in entries when no assistant is pending", () => { + FastCheck.assert( + FastCheck.property(word, (body) => { + const next = SessionEntryStepper.step( + memoryState(), + SessionEvent.Prompt.create({ text: body, timestamp: time(1) }), + ) + expect(next.entries).toHaveLength(1) + expect(next.entries[0]?.type).toBe("user") + if (next.entries[0]?.type !== "user") return + expect(next.entries[0].text).toBe(body) + }), + { numRuns: 50 }, + ) + }) - test("stores prompts in pending when an assistant is pending", () => { - FastCheck.assert( - FastCheck.property(word, (body) => { - const next = SessionEntryStepper.step(active(), SessionEvent.Prompt.create({ text: body, timestamp: time(1) })) - expect(next.pending).toHaveLength(1) - expect(next.pending[0]?.type).toBe("user") - if (next.pending[0]?.type !== "user") return - expect(next.pending[0].text).toBe(body) - }), - { numRuns: 50 }, - ) - }) + test("stores prompts in pending when an assistant is pending", () => { + FastCheck.assert( + FastCheck.property(word, (body) => { + const next = SessionEntryStepper.step( + active(), + SessionEvent.Prompt.create({ text: body, timestamp: time(1) }), + ) + expect(next.pending).toHaveLength(1) + expect(next.pending[0]?.type).toBe("user") + if (next.pending[0]?.type !== "user") return + expect(next.pending[0].text).toBe(body) + }), + { numRuns: 50 }, + ) + }) - test("accumulates text deltas on the latest text part", () => { - FastCheck.assert( - FastCheck.property(texts, (parts) => { - const next = parts.reduce( - (state, part, i) => - SessionEntryStepper.step(state, SessionEvent.Text.Delta.create({ delta: part, timestamp: time(i + 2) })), - SessionEntryStepper.step(active(), SessionEvent.Text.Started.create({ timestamp: time(1) })), - ) + test("accumulates text deltas on the latest text part", () => { + FastCheck.assert( + FastCheck.property(texts, (parts) => { + const next = parts.reduce( + (state, part, i) => + SessionEntryStepper.step( + state, + SessionEvent.Text.Delta.create({ delta: part, timestamp: time(i + 2) }), + ), + SessionEntryStepper.step(active(), SessionEvent.Text.Started.create({ timestamp: time(1) })), + ) - expect(texts_of(next)).toEqual([ - { - type: "text", - text: parts.join(""), + expect(texts_of(next)).toEqual([ + { + type: "text", + text: parts.join(""), + }, + ]) + }), + { numRuns: 100 }, + ) + }) + + test("routes later text deltas to the latest text segment", () => { + FastCheck.assert( + FastCheck.property(texts, texts, (a, b) => { + const next = run( + [ + SessionEvent.Text.Started.create({ timestamp: time(1) }), + ...a.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 2) })), + SessionEvent.Text.Started.create({ timestamp: time(a.length + 2) }), + ...b.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + a.length + 3) })), + ], + active(), + ) + + expect(texts_of(next)).toEqual([ + { type: "text", text: a.join("") }, + { type: "text", text: b.join("") }, + ]) + }), + { numRuns: 50 }, + ) + }) + + test("reasoning.ended replaces buffered reasoning text", () => { + FastCheck.assert( + FastCheck.property(texts, text, (parts, end) => { + const next = run( + [ + SessionEvent.Reasoning.Started.create({ timestamp: time(1) }), + ...parts.map((x, i) => SessionEvent.Reasoning.Delta.create({ delta: x, timestamp: time(i + 2) })), + SessionEvent.Reasoning.Ended.create({ text: end, timestamp: time(parts.length + 2) }), + ], + active(), + ) + + expect(reasons(next)).toEqual([ + { + type: "reasoning", + text: end, + }, + ]) + }), + { numRuns: 100 }, + ) + }) + + test("tool.success completes the latest running tool", () => { + FastCheck.assert( + FastCheck.property( + word, + word, + dict, + maybe(text), + maybe(dict), + maybe(files), + texts, + (callID, title, input, output, metadata, attachments, parts) => { + const next = run( + [ + SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }), + ...parts.map((x, i) => + SessionEvent.Tool.Input.Delta.create({ callID, delta: x, timestamp: time(i + 2) }), + ), + SessionEvent.Tool.Called.create({ + callID, + tool: "bash", + input, + provider: { executed: true }, + timestamp: time(parts.length + 2), + }), + SessionEvent.Tool.Success.create({ + callID, + title, + output, + metadata, + attachments, + provider: { executed: true }, + timestamp: time(parts.length + 3), + }), + ], + active(), + ) + + const match = tool(next, callID) + expect(match?.state.status).toBe("completed") + if (match?.state.status !== "completed") return + + expect(match.time.ran).toEqual(time(parts.length + 2)) + expect(match.state.input).toEqual(input) + expect(match.state.output).toBe(output ?? "") + expect(match.state.title).toBe(title) + expect(match.state.metadata).toEqual(metadata ?? {}) + expect(match.state.attachments).toEqual(attachments ?? []) }, - ]) - }), - { numRuns: 100 }, - ) - }) + ), + { numRuns: 50 }, + ) + }) - test("routes later text deltas to the latest text segment", () => { - FastCheck.assert( - FastCheck.property(texts, texts, (a, b) => { - const next = run( - [ - SessionEvent.Text.Started.create({ timestamp: time(1) }), - ...a.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 2) })), - SessionEvent.Text.Started.create({ timestamp: time(a.length + 2) }), - ...b.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + a.length + 3) })), - ], - active(), - ) - - expect(texts_of(next)).toEqual([ - { type: "text", text: a.join("") }, - { type: "text", text: b.join("") }, - ]) - }), - { numRuns: 50 }, - ) - }) - - test("reasoning.ended replaces buffered reasoning text", () => { - FastCheck.assert( - FastCheck.property(texts, text, (parts, end) => { - const next = run( - [ - SessionEvent.Reasoning.Started.create({ timestamp: time(1) }), - ...parts.map((x, i) => SessionEvent.Reasoning.Delta.create({ delta: x, timestamp: time(i + 2) })), - SessionEvent.Reasoning.Ended.create({ text: end, timestamp: time(parts.length + 2) }), - ], - active(), - ) - - expect(reasons(next)).toEqual([ - { - type: "reasoning", - text: end, - }, - ]) - }), - { numRuns: 100 }, - ) - }) - - test("tool.success completes the latest running tool", () => { - FastCheck.assert( - FastCheck.property( - word, - word, - dict, - maybe(text), - maybe(dict), - maybe(files), - texts, - (callID, title, input, output, metadata, attachments, parts) => { + test("tool.error completes the latest running tool with an error", () => { + FastCheck.assert( + FastCheck.property(word, dict, word, maybe(dict), (callID, input, error, metadata) => { const next = run( [ SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }), - ...parts.map((x, i) => - SessionEvent.Tool.Input.Delta.create({ callID, delta: x, timestamp: time(i + 2) }), - ), SessionEvent.Tool.Called.create({ callID, tool: "bash", input, provider: { executed: true }, - timestamp: time(parts.length + 2), + timestamp: time(2), }), - SessionEvent.Tool.Success.create({ + SessionEvent.Tool.Error.create({ callID, - title, - output, + error, metadata, - attachments, provider: { executed: true }, - timestamp: time(parts.length + 3), + timestamp: time(3), }), ], active(), ) const match = tool(next, callID) - expect(match?.state.status).toBe("completed") - if (match?.state.status !== "completed") return + expect(match?.state.status).toBe("error") + if (match?.state.status !== "error") return - expect(match.time.ran).toEqual(time(parts.length + 2)) + expect(match.time.ran).toEqual(time(2)) expect(match.state.input).toEqual(input) - expect(match.state.output).toBe(output ?? "") - expect(match.state.title).toBe(title) + expect(match.state.error).toBe(error) expect(match.state.metadata).toEqual(metadata ?? {}) - expect(match.state.attachments).toEqual(attachments ?? []) - }, - ), - { numRuns: 50 }, - ) - }) + }), + { numRuns: 50 }, + ) + }) - test("tool.error completes the latest running tool with an error", () => { - FastCheck.assert( - FastCheck.property(word, dict, word, maybe(dict), (callID, input, error, metadata) => { - const next = run( - [ - SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }), - SessionEvent.Tool.Called.create({ - callID, - tool: "bash", - input, - provider: { executed: true }, - timestamp: time(2), - }), - SessionEvent.Tool.Error.create({ - callID, - error, - metadata, - provider: { executed: true }, - timestamp: time(3), - }), - ], - active(), - ) + test("tool.success is ignored before tool.called promotes the tool to running", () => { + FastCheck.assert( + FastCheck.property(word, word, (callID, title) => { + const next = run( + [ + SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }), + SessionEvent.Tool.Success.create({ + callID, + title, + provider: { executed: true }, + timestamp: time(2), + }), + ], + active(), + ) + const match = tool(next, callID) + expect(match?.state).toEqual({ + status: "pending", + input: "", + }) + }), + { numRuns: 50 }, + ) + }) - const match = tool(next, callID) - expect(match?.state.status).toBe("error") - if (match?.state.status !== "error") return - - expect(match.time.ran).toEqual(time(2)) - expect(match.state.input).toEqual(input) - expect(match.state.error).toBe(error) - expect(match.state.metadata).toEqual(metadata ?? {}) - }), - { numRuns: 50 }, - ) - }) - - test("tool.success is ignored before tool.called promotes the tool to running", () => { - FastCheck.assert( - FastCheck.property(word, word, (callID, title) => { - const next = run( - [ - SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }), - SessionEvent.Tool.Success.create({ - callID, - title, - provider: { executed: true }, - timestamp: time(2), - }), - ], - active(), - ) - const match = tool(next, callID) - expect(match?.state).toEqual({ - status: "pending", - input: "", - }) - }), - { numRuns: 50 }, - ) - }) - - test("step.ended copies completion fields onto the pending assistant", () => { - FastCheck.assert( - FastCheck.property(FastCheck.integer({ min: 1, max: 1000 }), (n) => { - const event = SessionEvent.Step.Ended.create({ - reason: "stop", - cost: 1, - tokens: { - input: 1, - output: 2, - reasoning: 3, - cache: { - read: 4, - write: 5, + test("step.ended copies completion fields onto the pending assistant", () => { + FastCheck.assert( + FastCheck.property(FastCheck.integer({ min: 1, max: 1000 }), (n) => { + const event = SessionEvent.Step.Ended.create({ + reason: "stop", + cost: 1, + tokens: { + input: 1, + output: 2, + reasoning: 3, + cache: { + read: 4, + write: 5, + }, }, - }, - timestamp: time(n), - }) - const next = SessionEntryStepper.step(active(), event) - const entry = last(next) - expect(entry).toBeDefined() - if (!entry) return + timestamp: time(n), + }) + const next = SessionEntryStepper.step(active(), event) + const entry = last(next) + expect(entry).toBeDefined() + if (!entry) return - expect(entry.time.completed).toEqual(event.timestamp) - expect(entry.cost).toBe(event.cost) - expect(entry.tokens).toEqual(event.tokens) - }), - { numRuns: 50 }, - ) + expect(entry.time.completed).toEqual(event.timestamp) + expect(entry.cost).toBe(event.cost) + expect(entry.tokens).toEqual(event.tokens) + }), + { numRuns: 50 }, + ) + }) }) - }) describe("known reducer gaps", () => { - test("prompt appends immutably when no assistant is pending", () => { - FastCheck.assert( - FastCheck.property(word, (body) => { - const old = memoryState() - const next = SessionEntryStepper.step(old, SessionEvent.Prompt.create({ text: body, timestamp: time(1) })) - expect(old).not.toBe(next) - expect(old.entries).toHaveLength(0) - expect(next.entries).toHaveLength(1) - }), - { numRuns: 50 }, - ) - }) + test("prompt appends immutably when no assistant is pending", () => { + FastCheck.assert( + FastCheck.property(word, (body) => { + const old = memoryState() + const next = SessionEntryStepper.step(old, SessionEvent.Prompt.create({ text: body, timestamp: time(1) })) + expect(old).not.toBe(next) + expect(old.entries).toHaveLength(0) + expect(next.entries).toHaveLength(1) + }), + { numRuns: 50 }, + ) + }) - test("prompt appends immutably when an assistant is pending", () => { - FastCheck.assert( - FastCheck.property(word, (body) => { - const old = active() - const next = SessionEntryStepper.step(old, SessionEvent.Prompt.create({ text: body, timestamp: time(1) })) - expect(old).not.toBe(next) - expect(old.pending).toHaveLength(0) - expect(next.pending).toHaveLength(1) - }), - { numRuns: 50 }, - ) - }) + test("prompt appends immutably when an assistant is pending", () => { + FastCheck.assert( + FastCheck.property(word, (body) => { + const old = active() + const next = SessionEntryStepper.step(old, SessionEvent.Prompt.create({ text: body, timestamp: time(1) })) + expect(old).not.toBe(next) + expect(old.pending).toHaveLength(0) + expect(next.pending).toHaveLength(1) + }), + { numRuns: 50 }, + ) + }) - test("step.started creates an assistant consumed by follow-up events", () => { - FastCheck.assert( - FastCheck.property(texts, (parts) => { - const next = run([ - SessionEvent.Step.Started.create({ - model: { - id: "model", - providerID: "provider", - }, - timestamp: time(1), - }), - SessionEvent.Text.Started.create({ timestamp: time(2) }), - ...parts.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 3) })), - SessionEvent.Step.Ended.create({ - reason: "stop", - cost: 1, - tokens: { - input: 1, - output: 2, - reasoning: 3, - cache: { - read: 4, - write: 5, - }, - }, - timestamp: time(parts.length + 3), - }), - ]) - const entry = last(next) - - expect(entry).toBeDefined() - if (!entry) return - - expect(entry.content).toEqual([ - { - type: "text", - text: parts.join(""), - }, - ]) - expect(entry.time.completed).toEqual(time(parts.length + 3)) - }), - { numRuns: 100 }, - ) - }) - - test("replays prompt -> step -> text -> step.ended", () => { - FastCheck.assert( - FastCheck.property(word, texts, (body, parts) => { - const next = run([ - SessionEvent.Prompt.create({ text: body, timestamp: time(0) }), - SessionEvent.Step.Started.create({ - model: { - id: "model", - providerID: "provider", - }, - timestamp: time(1), - }), - SessionEvent.Text.Started.create({ timestamp: time(2) }), - ...parts.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 3) })), - SessionEvent.Step.Ended.create({ - reason: "stop", - cost: 1, - tokens: { - input: 1, - output: 2, - reasoning: 3, - cache: { - read: 4, - write: 5, - }, - }, - timestamp: time(parts.length + 3), - }), - ]) - - expect(next.entries).toHaveLength(2) - expect(next.entries[0]?.type).toBe("user") - expect(next.entries[1]?.type).toBe("assistant") - if (next.entries[1]?.type !== "assistant") return - - expect(next.entries[1].content).toEqual([ - { - type: "text", - text: parts.join(""), - }, - ]) - expect(next.entries[1].time.completed).toEqual(time(parts.length + 3)) - }), - { numRuns: 50 }, - ) - }) - - test("replays prompt -> step -> reasoning -> tool -> success -> step.ended", () => { - FastCheck.assert( - FastCheck.property( - word, - texts, - text, - dict, - word, - maybe(text), - maybe(dict), - maybe(files), - (body, reason, end, input, title, output, metadata, attachments) => { - const callID = "call" + test("step.started creates an assistant consumed by follow-up events", () => { + FastCheck.assert( + FastCheck.property(texts, (parts) => { const next = run([ - SessionEvent.Prompt.create({ text: body, timestamp: time(0) }), SessionEvent.Step.Started.create({ model: { id: "model", @@ -596,26 +519,8 @@ describe("session-entry-stepper", () => { }, timestamp: time(1), }), - SessionEvent.Reasoning.Started.create({ timestamp: time(2) }), - ...reason.map((x, i) => SessionEvent.Reasoning.Delta.create({ delta: x, timestamp: time(i + 3) })), - SessionEvent.Reasoning.Ended.create({ text: end, timestamp: time(reason.length + 3) }), - SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(reason.length + 4) }), - SessionEvent.Tool.Called.create({ - callID, - tool: "bash", - input, - provider: { executed: true }, - timestamp: time(reason.length + 5), - }), - SessionEvent.Tool.Success.create({ - callID, - title, - output, - metadata, - attachments, - provider: { executed: true }, - timestamp: time(reason.length + 6), - }), + SessionEvent.Text.Started.create({ timestamp: time(2) }), + ...parts.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 3) })), SessionEvent.Step.Ended.create({ reason: "stop", cost: 1, @@ -628,194 +533,318 @@ describe("session-entry-stepper", () => { write: 5, }, }, - timestamp: time(reason.length + 7), + timestamp: time(parts.length + 3), + }), + ]) + const entry = last(next) + + expect(entry).toBeDefined() + if (!entry) return + + expect(entry.content).toEqual([ + { + type: "text", + text: parts.join(""), + }, + ]) + expect(entry.time.completed).toEqual(time(parts.length + 3)) + }), + { numRuns: 100 }, + ) + }) + + test("replays prompt -> step -> text -> step.ended", () => { + FastCheck.assert( + FastCheck.property(word, texts, (body, parts) => { + const next = run([ + SessionEvent.Prompt.create({ text: body, timestamp: time(0) }), + SessionEvent.Step.Started.create({ + model: { + id: "model", + providerID: "provider", + }, + timestamp: time(1), + }), + SessionEvent.Text.Started.create({ timestamp: time(2) }), + ...parts.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 3) })), + SessionEvent.Step.Ended.create({ + reason: "stop", + cost: 1, + tokens: { + input: 1, + output: 2, + reasoning: 3, + cache: { + read: 4, + write: 5, + }, + }, + timestamp: time(parts.length + 3), }), ]) - expect(next.entries.at(-1)?.type).toBe("assistant") - const entry = next.entries.at(-1) - if (entry?.type !== "assistant") return + expect(next.entries).toHaveLength(2) + expect(next.entries[0]?.type).toBe("user") + expect(next.entries[1]?.type).toBe("assistant") + if (next.entries[1]?.type !== "assistant") return - expect(entry.content).toHaveLength(2) - expect(entry.content[0]).toEqual({ - type: "reasoning", - text: end, - }) - expect(entry.content[1]?.type).toBe("tool") - if (entry.content[1]?.type !== "tool") return - expect(entry.content[1].state.status).toBe("completed") - expect(entry.time.completed).toEqual(time(reason.length + 7)) - }, - ), - { numRuns: 50 }, - ) - }) - - test("starting a new step completes the old assistant and appends a new active assistant", () => { - const next = run( - [ - SessionEvent.Step.Started.create({ - model: { - id: "model", - providerID: "provider", - }, - timestamp: time(1), + expect(next.entries[1].content).toEqual([ + { + type: "text", + text: parts.join(""), + }, + ]) + expect(next.entries[1].time.completed).toEqual(time(parts.length + 3)) }), - ], - active(), - ) - expect(next.entries).toHaveLength(2) - expect(next.entries[0]?.type).toBe("assistant") - expect(next.entries[1]?.type).toBe("assistant") - if (next.entries[0]?.type !== "assistant" || next.entries[1]?.type !== "assistant") return + { numRuns: 50 }, + ) + }) - expect(next.entries[0].time.completed).toEqual(time(1)) - expect(next.entries[1].time.created).toEqual(time(1)) - expect(next.entries[1].time.completed).toBeUndefined() - }) + test("replays prompt -> step -> reasoning -> tool -> success -> step.ended", () => { + FastCheck.assert( + FastCheck.property( + word, + texts, + text, + dict, + word, + maybe(text), + maybe(dict), + maybe(files), + (body, reason, end, input, title, output, metadata, attachments) => { + const callID = "call" + const next = run([ + SessionEvent.Prompt.create({ text: body, timestamp: time(0) }), + SessionEvent.Step.Started.create({ + model: { + id: "model", + providerID: "provider", + }, + timestamp: time(1), + }), + SessionEvent.Reasoning.Started.create({ timestamp: time(2) }), + ...reason.map((x, i) => SessionEvent.Reasoning.Delta.create({ delta: x, timestamp: time(i + 3) })), + SessionEvent.Reasoning.Ended.create({ text: end, timestamp: time(reason.length + 3) }), + SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(reason.length + 4) }), + SessionEvent.Tool.Called.create({ + callID, + tool: "bash", + input, + provider: { executed: true }, + timestamp: time(reason.length + 5), + }), + SessionEvent.Tool.Success.create({ + callID, + title, + output, + metadata, + attachments, + provider: { executed: true }, + timestamp: time(reason.length + 6), + }), + SessionEvent.Step.Ended.create({ + reason: "stop", + cost: 1, + tokens: { + input: 1, + output: 2, + reasoning: 3, + cache: { + read: 4, + write: 5, + }, + }, + timestamp: time(reason.length + 7), + }), + ]) - test("handles sequential tools independently", () => { - FastCheck.assert( - FastCheck.property(dict, dict, word, word, (a, b, title, error) => { - const next = run( - [ - SessionEvent.Tool.Input.Started.create({ callID: "a", name: "bash", timestamp: time(1) }), - SessionEvent.Tool.Called.create({ - callID: "a", - tool: "bash", - input: a, - provider: { executed: true }, - timestamp: time(2), - }), - SessionEvent.Tool.Success.create({ - callID: "a", - title, - output: "done", - provider: { executed: true }, - timestamp: time(3), - }), - SessionEvent.Tool.Input.Started.create({ callID: "b", name: "grep", timestamp: time(4) }), - SessionEvent.Tool.Called.create({ - callID: "b", - tool: "bash", - input: b, - provider: { executed: true }, - timestamp: time(5), - }), - SessionEvent.Tool.Error.create({ - callID: "b", - error, - provider: { executed: true }, - timestamp: time(6), - }), - ], - active(), - ) + expect(next.entries.at(-1)?.type).toBe("assistant") + const entry = next.entries.at(-1) + if (entry?.type !== "assistant") return - const first = tool(next, "a") - const second = tool(next, "b") + expect(entry.content).toHaveLength(2) + expect(entry.content[0]).toEqual({ + type: "reasoning", + text: end, + }) + expect(entry.content[1]?.type).toBe("tool") + if (entry.content[1]?.type !== "tool") return + expect(entry.content[1].state.status).toBe("completed") + expect(entry.time.completed).toEqual(time(reason.length + 7)) + }, + ), + { numRuns: 50 }, + ) + }) - expect(first?.state.status).toBe("completed") - if (first?.state.status !== "completed") return - expect(first.state.input).toEqual(a) - expect(first.state.output).toBe("done") - expect(first.state.title).toBe(title) + test("starting a new step completes the old assistant and appends a new active assistant", () => { + const next = run( + [ + SessionEvent.Step.Started.create({ + model: { + id: "model", + providerID: "provider", + }, + timestamp: time(1), + }), + ], + active(), + ) + expect(next.entries).toHaveLength(2) + expect(next.entries[0]?.type).toBe("assistant") + expect(next.entries[1]?.type).toBe("assistant") + if (next.entries[0]?.type !== "assistant" || next.entries[1]?.type !== "assistant") return - expect(second?.state.status).toBe("error") - if (second?.state.status !== "error") return - expect(second.state.input).toEqual(b) - expect(second.state.error).toBe(error) - }), - { numRuns: 50 }, - ) - }) + expect(next.entries[0].time.completed).toEqual(time(1)) + expect(next.entries[1].time.created).toEqual(time(1)) + expect(next.entries[1].time.completed).toBeUndefined() + }) - test("routes tool events by callID when tool streams interleave", () => { - FastCheck.assert( - FastCheck.property(dict, dict, word, word, text, text, (a, b, titleA, titleB, deltaA, deltaB) => { - const next = run( - [ - SessionEvent.Tool.Input.Started.create({ callID: "a", name: "bash", timestamp: time(1) }), - SessionEvent.Tool.Input.Started.create({ callID: "b", name: "grep", timestamp: time(2) }), - SessionEvent.Tool.Input.Delta.create({ callID: "a", delta: deltaA, timestamp: time(3) }), - SessionEvent.Tool.Input.Delta.create({ callID: "b", delta: deltaB, timestamp: time(4) }), - SessionEvent.Tool.Called.create({ - callID: "a", - tool: "bash", - input: a, - provider: { executed: true }, - timestamp: time(5), - }), - SessionEvent.Tool.Called.create({ - callID: "b", - tool: "grep", - input: b, - provider: { executed: true }, - timestamp: time(6), - }), - SessionEvent.Tool.Success.create({ - callID: "a", - title: titleA, - output: "done-a", - provider: { executed: true }, - timestamp: time(7), - }), - SessionEvent.Tool.Success.create({ - callID: "b", - title: titleB, - output: "done-b", - provider: { executed: true }, - timestamp: time(8), - }), - ], - active(), - ) + test("handles sequential tools independently", () => { + FastCheck.assert( + FastCheck.property(dict, dict, word, word, (a, b, title, error) => { + const next = run( + [ + SessionEvent.Tool.Input.Started.create({ callID: "a", name: "bash", timestamp: time(1) }), + SessionEvent.Tool.Called.create({ + callID: "a", + tool: "bash", + input: a, + provider: { executed: true }, + timestamp: time(2), + }), + SessionEvent.Tool.Success.create({ + callID: "a", + title, + output: "done", + provider: { executed: true }, + timestamp: time(3), + }), + SessionEvent.Tool.Input.Started.create({ callID: "b", name: "grep", timestamp: time(4) }), + SessionEvent.Tool.Called.create({ + callID: "b", + tool: "bash", + input: b, + provider: { executed: true }, + timestamp: time(5), + }), + SessionEvent.Tool.Error.create({ + callID: "b", + error, + provider: { executed: true }, + timestamp: time(6), + }), + ], + active(), + ) - const first = tool(next, "a") - const second = tool(next, "b") + const first = tool(next, "a") + const second = tool(next, "b") - expect(first?.state.status).toBe("completed") - expect(second?.state.status).toBe("completed") - if (first?.state.status !== "completed" || second?.state.status !== "completed") return + expect(first?.state.status).toBe("completed") + if (first?.state.status !== "completed") return + expect(first.state.input).toEqual(a) + expect(first.state.output).toBe("done") + expect(first.state.title).toBe(title) - expect(first.state.input).toEqual(a) - expect(second.state.input).toEqual(b) - expect(first.state.title).toBe(titleA) - expect(second.state.title).toBe(titleB) - }), - { numRuns: 50 }, - ) - }) + expect(second?.state.status).toBe("error") + if (second?.state.status !== "error") return + expect(second.state.input).toEqual(b) + expect(second.state.error).toBe(error) + }), + { numRuns: 50 }, + ) + }) - test("records synthetic events", () => { - FastCheck.assert( - FastCheck.property(word, (body) => { - const next = SessionEntryStepper.step(memoryState(), SessionEvent.Synthetic.create({ text: body, timestamp: time(1) })) - expect(next.entries).toHaveLength(1) - expect(next.entries[0]?.type).toBe("synthetic") - if (next.entries[0]?.type !== "synthetic") return - expect(next.entries[0].text).toBe(body) - }), - { numRuns: 50 }, - ) - }) + test("routes tool events by callID when tool streams interleave", () => { + FastCheck.assert( + FastCheck.property(dict, dict, word, word, text, text, (a, b, titleA, titleB, deltaA, deltaB) => { + const next = run( + [ + SessionEvent.Tool.Input.Started.create({ callID: "a", name: "bash", timestamp: time(1) }), + SessionEvent.Tool.Input.Started.create({ callID: "b", name: "grep", timestamp: time(2) }), + SessionEvent.Tool.Input.Delta.create({ callID: "a", delta: deltaA, timestamp: time(3) }), + SessionEvent.Tool.Input.Delta.create({ callID: "b", delta: deltaB, timestamp: time(4) }), + SessionEvent.Tool.Called.create({ + callID: "a", + tool: "bash", + input: a, + provider: { executed: true }, + timestamp: time(5), + }), + SessionEvent.Tool.Called.create({ + callID: "b", + tool: "grep", + input: b, + provider: { executed: true }, + timestamp: time(6), + }), + SessionEvent.Tool.Success.create({ + callID: "a", + title: titleA, + output: "done-a", + provider: { executed: true }, + timestamp: time(7), + }), + SessionEvent.Tool.Success.create({ + callID: "b", + title: titleB, + output: "done-b", + provider: { executed: true }, + timestamp: time(8), + }), + ], + active(), + ) - test("records compaction events", () => { - FastCheck.assert( - FastCheck.property(FastCheck.boolean(), maybe(FastCheck.boolean()), (auto, overflow) => { - const next = SessionEntryStepper.step( - memoryState(), - SessionEvent.Compacted.create({ auto, overflow, timestamp: time(1) }), - ) - expect(next.entries).toHaveLength(1) - expect(next.entries[0]?.type).toBe("compaction") - if (next.entries[0]?.type !== "compaction") return - expect(next.entries[0].auto).toBe(auto) - expect(next.entries[0].overflow).toBe(overflow) - }), - { numRuns: 50 }, - ) - }) + const first = tool(next, "a") + const second = tool(next, "b") + + expect(first?.state.status).toBe("completed") + expect(second?.state.status).toBe("completed") + if (first?.state.status !== "completed" || second?.state.status !== "completed") return + + expect(first.state.input).toEqual(a) + expect(second.state.input).toEqual(b) + expect(first.state.title).toBe(titleA) + expect(second.state.title).toBe(titleB) + }), + { numRuns: 50 }, + ) + }) + + test("records synthetic events", () => { + FastCheck.assert( + FastCheck.property(word, (body) => { + const next = SessionEntryStepper.step( + memoryState(), + SessionEvent.Synthetic.create({ text: body, timestamp: time(1) }), + ) + expect(next.entries).toHaveLength(1) + expect(next.entries[0]?.type).toBe("synthetic") + if (next.entries[0]?.type !== "synthetic") return + expect(next.entries[0].text).toBe(body) + }), + { numRuns: 50 }, + ) + }) + + test("records compaction events", () => { + FastCheck.assert( + FastCheck.property(FastCheck.boolean(), maybe(FastCheck.boolean()), (auto, overflow) => { + const next = SessionEntryStepper.step( + memoryState(), + SessionEvent.Compacted.create({ auto, overflow, timestamp: time(1) }), + ) + expect(next.entries).toHaveLength(1) + expect(next.entries[0]?.type).toBe("compaction") + if (next.entries[0]?.type !== "compaction") return + expect(next.entries[0].auto).toBe(auto) + expect(next.entries[0].overflow).toBe(overflow) + }), + { numRuns: 50 }, + ) + }) }) }) }) From 882b8e1e7587c4b24e5cb7ee9409e93b9455c5b0 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 18 Apr 2026 10:35:25 -0400 Subject: [PATCH 185/335] core: track retry attempts with detailed error context on assistant entries users can now see when transient failures occur during assistant responses, such as rate limits or provider overloads, giving visibility into what issues were encountered and automatically resolved before the final response --- .../opencode/src/v2/session-entry-stepper.ts | 12 ++- packages/opencode/src/v2/session-entry.ts | 20 +++++ packages/opencode/src/v2/session-event.ts | 15 +++- .../session/session-entry-stepper.test.ts | 87 +++++++++++++++++++ 4 files changed, 130 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/v2/session-entry-stepper.ts b/packages/opencode/src/v2/session-entry-stepper.ts index 3d642579d0..3fe4266c04 100644 --- a/packages/opencode/src/v2/session-entry-stepper.ts +++ b/packages/opencode/src/v2/session-entry-stepper.ts @@ -1,4 +1,4 @@ -import { castDraft, produce, type WritableDraft } from "immer" +import { produce, type WritableDraft } from "immer" import { SessionEvent } from "./session-event" import { SessionEntry } from "./session-entry" @@ -235,7 +235,15 @@ export function stepWith(adapter: Adapter, event: SessionEvent.E ) } }, - retried: () => {}, + retried: (event) => { + if (currentAssistant) { + adapter.updateAssistant( + produce(currentAssistant, (draft) => { + draft.retries = [...(draft.retries ?? []), SessionEntry.AssistantRetry.fromEvent(event)] + }), + ) + } + }, compacted: (event) => { adapter.appendEntry(SessionEntry.Compaction.fromEvent(event)) }, diff --git a/packages/opencode/src/v2/session-entry.ts b/packages/opencode/src/v2/session-entry.ts index 97c5fc7ce9..b261d8b5b2 100644 --- a/packages/opencode/src/v2/session-entry.ts +++ b/packages/opencode/src/v2/session-entry.ts @@ -104,6 +104,24 @@ export class AssistantReasoning extends Schema.Class("Sessio text: Schema.String, }) {} +export class AssistantRetry extends Schema.Class("Session.Entry.Assistant.Retry")({ + attempt: Schema.Number, + error: SessionEvent.RetryError, + time: Schema.Struct({ + created: Schema.DateTimeUtc, + }), +}) { + static fromEvent(event: SessionEvent.Retried) { + return new AssistantRetry({ + attempt: event.attempt, + error: event.error, + time: { + created: event.timestamp, + }, + }) + } +} + export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]).pipe( Schema.toTaggedUnion("type"), ) @@ -113,6 +131,7 @@ export class Assistant extends Schema.Class("Session.Entry.Assistant" ...Base, type: Schema.Literal("assistant"), content: AssistantContent.pipe(Schema.Array), + retries: AssistantRetry.pipe(Schema.Array, Schema.optional), cost: Schema.Number.pipe(Schema.optional), tokens: Schema.Struct({ input: Schema.Number, @@ -137,6 +156,7 @@ export class Assistant extends Schema.Class("Session.Entry.Assistant" created: event.timestamp, }, content: [], + retries: [], }) } } diff --git a/packages/opencode/src/v2/session-event.ts b/packages/opencode/src/v2/session-event.ts index 11d4a5db2d..f922becf3a 100644 --- a/packages/opencode/src/v2/session-event.ts +++ b/packages/opencode/src/v2/session-event.ts @@ -53,6 +53,15 @@ export namespace SessionEvent { source: Source.pipe(Schema.optional), }) {} + export class RetryError extends Schema.Class("Session.Event.Retry.Error")({ + message: Schema.String, + statusCode: Schema.Number.pipe(Schema.optional), + isRetryable: Schema.Boolean, + responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + responseBody: Schema.String.pipe(Schema.optional), + metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + }) {} + export class Prompt extends Schema.Class("Session.Event.Prompt")({ ...Base, type: Schema.Literal("prompt"), @@ -386,14 +395,16 @@ export namespace SessionEvent { export class Retried extends Schema.Class("Session.Event.Retried")({ ...Base, type: Schema.Literal("retried"), - error: Schema.String, + attempt: Schema.Number, + error: RetryError, }) { - static create(input: BaseInput & { error: string }) { + static create(input: BaseInput & { attempt: number; error: RetryError }) { return new Retried({ id: input.id ?? ID.create(), type: "retried", timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), metadata: input.metadata, + attempt: input.attempt, error: input.error, }) } diff --git a/packages/opencode/test/session/session-entry-stepper.test.ts b/packages/opencode/test/session/session-entry-stepper.test.ts index 5c7df2dbad..32036cb1e8 100644 --- a/packages/opencode/test/session/session-entry-stepper.test.ts +++ b/packages/opencode/test/session/session-entry-stepper.test.ts @@ -27,6 +27,24 @@ function assistant() { type: "assistant", time: { created: time(0) }, content: [], + retries: [], + }) +} + +function retryError(message: string) { + return new SessionEvent.RetryError({ + message, + isRetryable: true, + }) +} + +function retry(attempt: number, message: string, created: number) { + return new SessionEntry.AssistantRetry({ + attempt, + error: retryError(message), + time: { + created: time(created), + }, }) } @@ -78,6 +96,12 @@ function tool(state: SessionEntryStepper.MemoryState, callID: string) { return tools(state).find((x) => x.callID === callID) } +function retriesOf(state: SessionEntryStepper.MemoryState) { + const entry = last(state) + if (!entry) return [] + return entry.retries ?? [] +} + function adapterStore() { return { committed: [] as SessionEntry.Entry[], @@ -168,6 +192,33 @@ describe("session-entry-stepper", () => { ]) expect(store.committed[0].time.completed).toEqual(time(7)) }) + + test("aggregates retry events onto the current assistant", () => { + const store = adapterStore() + store.committed.push(assistant()) + + SessionEntryStepper.stepWith( + adapterFor(store), + SessionEvent.Retried.create({ + attempt: 1, + error: retryError("rate limited"), + timestamp: time(1), + }), + ) + SessionEntryStepper.stepWith( + adapterFor(store), + SessionEvent.Retried.create({ + attempt: 2, + error: retryError("provider overloaded"), + timestamp: time(2), + }), + ) + + expect(store.committed[0]?.type).toBe("assistant") + if (store.committed[0]?.type !== "assistant") return + + expect(store.committed[0].retries).toEqual([retry(1, "rate limited", 1), retry(2, "provider overloaded", 2)]) + }) }) describe("memory", () => { @@ -231,6 +282,21 @@ describe("session-entry-stepper", () => { expect(reasons(state)).toEqual([{ type: "reasoning", text: "final" }]) }) + + test("stepWith through memory records retries", () => { + const state = active() + + SessionEntryStepper.stepWith( + SessionEntryStepper.memory(state), + SessionEvent.Retried.create({ + attempt: 1, + error: retryError("rate limited"), + timestamp: time(1), + }), + ) + + expect(retriesOf(state)).toEqual([retry(1, "rate limited", 1)]) + }) }) describe("step", () => { @@ -481,6 +547,27 @@ describe("session-entry-stepper", () => { }) }) + test("records retries on the pending assistant", () => { + const next = run( + [ + SessionEvent.Retried.create({ + attempt: 1, + error: retryError("rate limited"), + timestamp: time(1), + }), + SessionEvent.Retried.create({ + attempt: 2, + error: retryError("provider overloaded"), + timestamp: time(2), + }), + ], + active(), + ) + + expect(retriesOf(next)).toEqual([retry(1, "rate limited", 1), retry(2, "provider overloaded", 2)]) + }) + }) + describe("known reducer gaps", () => { test("prompt appends immutably when no assistant is pending", () => { FastCheck.assert( From 55315bdffaae45d8a983b90836308ad460fc45e4 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 18 Apr 2026 10:37:33 -0400 Subject: [PATCH 186/335] tui: fix sync loading indicator to properly show loading state on startup --- .../opencode/src/cli/cmd/tui/context/sync.tsx | 1 - .../session/session-entry-stepper.test.ts | 21 ------------------- 2 files changed, 22 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index d2a7e5c4d0..57326e3a1a 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -467,7 +467,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ return store.status }, get ready() { - return true if (process.env.OPENCODE_FAST_BOOT) return true return store.status !== "loading" }, diff --git a/packages/opencode/test/session/session-entry-stepper.test.ts b/packages/opencode/test/session/session-entry-stepper.test.ts index 32036cb1e8..defce40c14 100644 --- a/packages/opencode/test/session/session-entry-stepper.test.ts +++ b/packages/opencode/test/session/session-entry-stepper.test.ts @@ -547,27 +547,6 @@ describe("session-entry-stepper", () => { }) }) - test("records retries on the pending assistant", () => { - const next = run( - [ - SessionEvent.Retried.create({ - attempt: 1, - error: retryError("rate limited"), - timestamp: time(1), - }), - SessionEvent.Retried.create({ - attempt: 2, - error: retryError("provider overloaded"), - timestamp: time(2), - }), - ], - active(), - ) - - expect(retriesOf(next)).toEqual([retry(1, "rate limited", 1), retry(2, "provider overloaded", 2)]) - }) - }) - describe("known reducer gaps", () => { test("prompt appends immutably when no assistant is pending", () => { FastCheck.assert( From 1ee712e549f8e31e38d70abd600ed48010659f8a Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 18 Apr 2026 10:42:33 -0400 Subject: [PATCH 187/335] core: fix early return when node_modules is missing during package install --- packages/opencode/src/npm/index.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/npm/index.ts b/packages/opencode/src/npm/index.ts index d92099bc3c..0760e768b9 100644 --- a/packages/opencode/src/npm/index.ts +++ b/packages/opencode/src/npm/index.ts @@ -150,13 +150,17 @@ export const layer = Layer.effect( if (!canWrite) return const add = input?.add.map((pkg) => [pkg.name, pkg.version].filter(Boolean).join("@")) ?? [] - yield* Effect.gen(function* () { - const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules")) - if (!nodeModulesExists) { - yield* reify({ add, dir }) - return - } - }).pipe(Effect.withSpan("Npm.checkNodeModules")) + if ( + yield* Effect.gen(function* () { + const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules")) + if (!nodeModulesExists) { + yield* reify({ add, dir }) + return true + } + return false + }).pipe(Effect.withSpan("Npm.checkNodeModules")) + ) + return yield* Effect.gen(function* () { const pkg = yield* afs.readJson(path.join(dir, "package.json")).pipe(Effect.orElseSucceed(() => ({}))) From 078d8a07cf848a7dd8067121d7c21c00d717b9d6 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 18 Apr 2026 11:20:29 -0400 Subject: [PATCH 188/335] core: support OTEL_RESOURCE_ATTRIBUTES environment variable for custom telemetry attributes Users can now pass custom OpenTelemetry resource attributes via the OTEL_RESOURCE_ATTRIBUTES environment variable (comma-separated key=value format). These attributes are automatically included in all telemetry data sent from both the main process and workspace environments, enabling better observability integration with existing monitoring systems that rely on custom resource tags. --- .../opencode/src/control-plane/workspace.ts | 1 + packages/opencode/src/effect/observability.ts | 19 +++++++- .../test/effect/observability.test.ts | 45 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 packages/opencode/test/effect/observability.test.ts diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index e94d6c2c93..eb689df025 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -117,6 +117,7 @@ export const create = fn(CreateInput, async (input) => { OPENCODE_EXPERIMENTAL_WORKSPACES: "true", OTEL_EXPORTER_OTLP_HEADERS: process.env.OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES, } await adaptor.create(config, env) diff --git a/packages/opencode/src/effect/observability.ts b/packages/opencode/src/effect/observability.ts index 1c385d60ae..fd719fd353 100644 --- a/packages/opencode/src/effect/observability.ts +++ b/packages/opencode/src/effect/observability.ts @@ -21,12 +21,29 @@ const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS ) : undefined -function resource() { +export function resource(): { serviceName: string, serviceVersion: string, attributes: Record } { const processMetadata = ensureProcessMetadata("main") + const attributes: Record = (() => { + const value = process.env.OTEL_RESOURCE_ATTRIBUTES + if (!value) return {} + try { + return Object.fromEntries( + value.split(",").map((entry) => { + const index = entry.indexOf("=") + if (index < 1) throw new Error("Invalid OTEL_RESOURCE_ATTRIBUTES entry") + return [decodeURIComponent(entry.slice(0, index)), decodeURIComponent(entry.slice(index + 1))] + }), + ) + } catch { + return {} + } + })() + return { serviceName: "opencode", serviceVersion: InstallationVersion, attributes: { + ...attributes, "deployment.environment.name": InstallationChannel, "opencode.client": Flag.OPENCODE_CLIENT, "opencode.process_role": processMetadata.processRole, diff --git a/packages/opencode/test/effect/observability.test.ts b/packages/opencode/test/effect/observability.test.ts new file mode 100644 index 0000000000..dd380a2de7 --- /dev/null +++ b/packages/opencode/test/effect/observability.test.ts @@ -0,0 +1,45 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { resource } from "../../src/effect/observability" + +const otelResourceAttributes = process.env.OTEL_RESOURCE_ATTRIBUTES +const opencodeClient = process.env.OPENCODE_CLIENT + +afterEach(() => { + if (otelResourceAttributes === undefined) delete process.env.OTEL_RESOURCE_ATTRIBUTES + else process.env.OTEL_RESOURCE_ATTRIBUTES = otelResourceAttributes + + if (opencodeClient === undefined) delete process.env.OPENCODE_CLIENT + else process.env.OPENCODE_CLIENT = opencodeClient +}) + +describe("resource", () => { + test("parses and decodes OTEL resource attributes", () => { + process.env.OTEL_RESOURCE_ATTRIBUTES = + "service.namespace=anomalyco,team=platform%2Cobservability,label=hello%3Dworld,key%2Fname=value%20here" + + expect(resource().attributes).toMatchObject({ + "service.namespace": "anomalyco", + team: "platform,observability", + label: "hello=world", + "key/name": "value here", + }) + }) + + test("drops OTEL resource attributes when any entry is invalid", () => { + process.env.OTEL_RESOURCE_ATTRIBUTES = "service.namespace=anomalyco,broken" + + expect(resource().attributes["service.namespace"]).toBeUndefined() + expect(resource().attributes["opencode.client"]).toBeDefined() + }) + + test("keeps built-in attributes when env values conflict", () => { + process.env.OPENCODE_CLIENT = "cli" + process.env.OTEL_RESOURCE_ATTRIBUTES = "opencode.client=web,service.instance.id=override,service.namespace=anomalyco" + + expect(resource().attributes).toMatchObject({ + "opencode.client": "cli", + "service.namespace": "anomalyco", + }) + expect(resource().attributes["service.instance.id"]).not.toBe("override") + }) +}) From dd8c424806dd2c78c35205566824017ffa3a37fa Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 18 Apr 2026 15:21:48 +0000 Subject: [PATCH 189/335] chore: generate --- packages/opencode/src/effect/observability.ts | 2 +- packages/opencode/test/effect/observability.test.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/effect/observability.ts b/packages/opencode/src/effect/observability.ts index fd719fd353..fb81d5f5b5 100644 --- a/packages/opencode/src/effect/observability.ts +++ b/packages/opencode/src/effect/observability.ts @@ -21,7 +21,7 @@ const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS ) : undefined -export function resource(): { serviceName: string, serviceVersion: string, attributes: Record } { +export function resource(): { serviceName: string; serviceVersion: string; attributes: Record } { const processMetadata = ensureProcessMetadata("main") const attributes: Record = (() => { const value = process.env.OTEL_RESOURCE_ATTRIBUTES diff --git a/packages/opencode/test/effect/observability.test.ts b/packages/opencode/test/effect/observability.test.ts index dd380a2de7..d062202827 100644 --- a/packages/opencode/test/effect/observability.test.ts +++ b/packages/opencode/test/effect/observability.test.ts @@ -34,7 +34,8 @@ describe("resource", () => { test("keeps built-in attributes when env values conflict", () => { process.env.OPENCODE_CLIENT = "cli" - process.env.OTEL_RESOURCE_ATTRIBUTES = "opencode.client=web,service.instance.id=override,service.namespace=anomalyco" + process.env.OTEL_RESOURCE_ATTRIBUTES = + "opencode.client=web,service.instance.id=override,service.namespace=anomalyco" expect(resource().attributes).toMatchObject({ "opencode.client": "cli", From 9918f389e7ffe0b5b6e6327045ecd6559775f7c6 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 18 Apr 2026 11:59:08 -0400 Subject: [PATCH 190/335] fix: detect attachment mime from file contents (#23291) --- packages/opencode/src/session/message-v2.ts | 6 +- packages/opencode/src/tool/read.ts | 137 +++++++++--------- packages/opencode/src/tool/webfetch.ts | 6 +- packages/opencode/src/util/media.ts | 29 ++++ .../opencode/test/session/message-v2.test.ts | 84 +++++++++++ packages/opencode/test/tool/read.test.ts | 13 ++ 6 files changed, 198 insertions(+), 77 deletions(-) create mode 100644 packages/opencode/src/util/media.ts diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 46686947e1..057b5eb66a 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -11,6 +11,7 @@ import { MessageTable, PartTable, SessionTable } from "./session.sql" import { ProviderError } from "@/provider" import { iife } from "@/util/iife" import { errorMessage } from "@/util/error" +import { isMedia } from "@/util/media" import type { SystemError } from "bun" import type { Provider } from "@/provider" import { ModelID, ProviderID } from "@/provider/schema" @@ -25,10 +26,7 @@ interface FetchDecompressionError extends Error { } export const SYNTHETIC_ATTACHMENT_PROMPT = "Attached image(s) from tool result:" - -export function isMedia(mime: string) { - return mime.startsWith("image/") || mime === "application/pdf" -} +export { isMedia } export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({})) export const AbortedError = NamedError.create("MessageAbortedError", z.object({ message: z.string() })) diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 18c668ca07..29d36692c6 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -1,7 +1,6 @@ import z from "zod" -import { Effect, Scope } from "effect" +import { Effect, Option, Scope } from "effect" import { createReadStream } from "fs" -import { open } from "fs/promises" import * as path from "path" import { createInterface } from "readline" import * as Tool from "./tool" @@ -11,12 +10,14 @@ import DESCRIPTION from "./read.txt" import { Instance } from "../project/instance" import { assertExternalDirectoryEffect } from "./external-directory" import { Instruction } from "../session/instruction" +import { isImageAttachment, isPdfAttachment, sniffAttachmentMime } from "@/util/media" const DEFAULT_READ_LIMIT = 2000 const MAX_LINE_LENGTH = 2000 const MAX_LINE_SUFFIX = `... (line truncated to ${MAX_LINE_LENGTH} chars)` const MAX_BYTES = 50 * 1024 const MAX_BYTES_LABEL = `${MAX_BYTES / 1024} KB` +const SAMPLE_BYTES = 4096 const parameters = z.object({ filePath: z.string().describe("The absolute path to the file or directory to read"), @@ -77,6 +78,64 @@ export const ReadTool = Tool.define( yield* lsp.touchFile(filepath, false).pipe(Effect.ignore, Effect.forkIn(scope)) }) + const readSample = Effect.fn("ReadTool.readSample")(function* (filepath: string, fileSize: number, sampleSize: number) { + if (fileSize === 0) return new Uint8Array() + + return yield* Effect.scoped( + Effect.gen(function* () { + const file = yield* fs.open(filepath, { flag: "r" }) + return Option.getOrElse(yield* file.readAlloc(Math.min(sampleSize, fileSize)), () => new Uint8Array()) + }), + ) + }) + + const isBinaryFile = (filepath: string, bytes: Uint8Array) => { + const ext = path.extname(filepath).toLowerCase() + switch (ext) { + case ".zip": + case ".tar": + case ".gz": + case ".exe": + case ".dll": + case ".so": + case ".class": + case ".jar": + case ".war": + case ".7z": + case ".doc": + case ".docx": + case ".xls": + case ".xlsx": + case ".ppt": + case ".pptx": + case ".odt": + case ".ods": + case ".odp": + case ".bin": + case ".dat": + case ".obj": + case ".o": + case ".a": + case ".lib": + case ".wasm": + case ".pyc": + case ".pyo": + return true + } + + if (bytes.length === 0) return false + + let nonPrintableCount = 0 + for (let i = 0; i < bytes.length; i++) { + if (bytes[i] === 0) return true + if (bytes[i] < 9 || (bytes[i] > 13 && bytes[i] < 32)) { + nonPrintableCount++ + } + } + + return nonPrintableCount / bytes.length > 0.3 + } + const run = Effect.fn("ReadTool.execute")(function* (params: z.infer, ctx: Tool.Context) { if (params.offset !== undefined && params.offset < 1) { return yield* Effect.fail(new Error("offset must be greater than or equal to 1")) @@ -141,12 +200,12 @@ export const ReadTool = Tool.define( } const loaded = yield* instruction.resolve(ctx.messages, filepath, ctx.messageID) + const sample = yield* readSample(filepath, Number(stat.size), SAMPLE_BYTES) - const mime = AppFileSystem.mimeType(filepath) - const isImage = mime.startsWith("image/") && mime !== "image/svg+xml" && mime !== "image/vnd.fastbidsheet" - const isPdf = mime === "application/pdf" - if (isImage || isPdf) { - const msg = `${isImage ? "Image" : "PDF"} read successfully` + const mime = sniffAttachmentMime(sample, AppFileSystem.mimeType(filepath)) + if (isImageAttachment(mime) || isPdfAttachment(mime)) { + const bytes = yield* fs.readFile(filepath) + const msg = isPdfAttachment(mime) ? "PDF read successfully" : "Image read successfully" return { title, output: msg, @@ -159,13 +218,13 @@ export const ReadTool = Tool.define( { type: "file" as const, mime, - url: `data:${mime};base64,${Buffer.from(yield* fs.readFile(filepath)).toString("base64")}`, + url: `data:${mime};base64,${Buffer.from(bytes).toString("base64")}`, }, ], } } - if (yield* Effect.promise(() => isBinaryFile(filepath, Number(stat.size)))) { + if (isBinaryFile(filepath, sample)) { return yield* Effect.fail(new Error(`Cannot read binary file: ${filepath}`)) } @@ -261,63 +320,3 @@ async function lines(filepath: string, opts: { limit: number; offset: number }) return { raw, count, cut, more, offset: opts.offset } } - -async function isBinaryFile(filepath: string, fileSize: number): Promise { - const ext = path.extname(filepath).toLowerCase() - // binary check for common non-text extensions - switch (ext) { - case ".zip": - case ".tar": - case ".gz": - case ".exe": - case ".dll": - case ".so": - case ".class": - case ".jar": - case ".war": - case ".7z": - case ".doc": - case ".docx": - case ".xls": - case ".xlsx": - case ".ppt": - case ".pptx": - case ".odt": - case ".ods": - case ".odp": - case ".bin": - case ".dat": - case ".obj": - case ".o": - case ".a": - case ".lib": - case ".wasm": - case ".pyc": - case ".pyo": - return true - default: - break - } - - if (fileSize === 0) return false - - const fh = await open(filepath, "r") - try { - const sampleSize = Math.min(4096, fileSize) - const bytes = Buffer.alloc(sampleSize) - const result = await fh.read(bytes, 0, sampleSize, 0) - if (result.bytesRead === 0) return false - - let nonPrintableCount = 0 - for (let i = 0; i < result.bytesRead; i++) { - if (bytes[i] === 0) return true - if (bytes[i] < 9 || (bytes[i] > 13 && bytes[i] < 32)) { - nonPrintableCount++ - } - } - // If >30% non-printable characters, consider it binary - return nonPrintableCount / result.bytesRead > 0.3 - } finally { - await fh.close() - } -} diff --git a/packages/opencode/src/tool/webfetch.ts b/packages/opencode/src/tool/webfetch.ts index 6498b871f8..1d988b8d4f 100644 --- a/packages/opencode/src/tool/webfetch.ts +++ b/packages/opencode/src/tool/webfetch.ts @@ -4,6 +4,7 @@ import { HttpClient, HttpClientRequest } from "effect/unstable/http" import * as Tool from "./tool" import TurndownService from "turndown" import DESCRIPTION from "./webfetch.txt" +import { isImageAttachment } from "@/util/media" const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds @@ -104,10 +105,7 @@ export const WebFetchTool = Tool.define( const mime = contentType.split(";")[0]?.trim().toLowerCase() || "" const title = `${params.url} (${contentType})` - // Check if response is an image - const isImage = mime.startsWith("image/") && mime !== "image/svg+xml" && mime !== "image/vnd.fastbidsheet" - - if (isImage) { + if (isImageAttachment(mime)) { const base64Content = Buffer.from(arrayBuffer).toString("base64") return { title, diff --git a/packages/opencode/src/util/media.ts b/packages/opencode/src/util/media.ts new file mode 100644 index 0000000000..0e98f53a52 --- /dev/null +++ b/packages/opencode/src/util/media.ts @@ -0,0 +1,29 @@ +const startsWith = (bytes: Uint8Array, prefix: number[]) => prefix.every((value, index) => bytes[index] === value) + +export function isPdfAttachment(mime: string) { + return mime === "application/pdf" +} + +export function isMedia(mime: string) { + return mime.startsWith("image/") || isPdfAttachment(mime) +} + +export function isImageAttachment(mime: string) { + return mime.startsWith("image/") && mime !== "image/svg+xml" && mime !== "image/vnd.fastbidsheet" +} + +export function sniffAttachmentMime(bytes: Uint8Array, fallback: string) { + if (startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return "image/png" + if (startsWith(bytes, [0xff, 0xd8, 0xff])) return "image/jpeg" + if (startsWith(bytes, [0x47, 0x49, 0x46, 0x38])) return "image/gif" + if (startsWith(bytes, [0x42, 0x4d])) return "image/bmp" + if (startsWith(bytes, [0x25, 0x50, 0x44, 0x46, 0x2d])) return "application/pdf" + if ( + startsWith(bytes, [0x52, 0x49, 0x46, 0x46]) && + startsWith(bytes.subarray(8), [0x57, 0x45, 0x42, 0x50]) + ) { + return "image/webp" + } + + return fallback +} diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 6d4e994a87..55ae65c560 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test" import { APICallError } from "ai" import { MessageV2 } from "../../src/session/message-v2" +import { ProviderTransform } from "../../src/provider" import type { Provider } from "../../src/provider" import { ModelID, ProviderID } from "../../src/provider/schema" import { SessionID, MessageID, PartID } from "../../src/session/schema" @@ -359,6 +360,89 @@ describe("session.message-v2.toModelMessage", () => { ]) }) + test("preserves jpeg tool-result media for anthropic models", async () => { + const anthropicModel: Provider.Model = { + ...model, + id: ModelID.make("anthropic/claude-opus-4-7"), + providerID: ProviderID.make("anthropic"), + api: { + id: "claude-opus-4-7-20250805", + url: "https://api.anthropic.com", + npm: "@ai-sdk/anthropic", + }, + capabilities: { + ...model.capabilities, + attachment: true, + input: { + ...model.capabilities.input, + image: true, + pdf: true, + }, + }, + } + const jpeg = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]).toString( + "base64", + ) + const userID = "m-user-anthropic" + const assistantID = "m-assistant-anthropic" + const input: MessageV2.WithParts[] = [ + { + info: userInfo(userID), + parts: [ + { + ...basePart(userID, "u1-anthropic"), + type: "text", + text: "run tool", + }, + ] as MessageV2.Part[], + }, + { + info: assistantInfo(assistantID, userID), + parts: [ + { + ...basePart(assistantID, "a1-anthropic"), + type: "tool", + callID: "call-anthropic-1", + tool: "read", + state: { + status: "completed", + input: { filePath: "/tmp/rails-demo.png" }, + output: "Image read successfully", + title: "Read", + metadata: {}, + time: { start: 0, end: 1 }, + attachments: [ + { + ...basePart(assistantID, "file-anthropic-1"), + type: "file", + mime: "image/jpeg", + filename: "rails-demo.png", + url: `data:image/jpeg;base64,${jpeg}`, + }, + ], + }, + }, + ] as MessageV2.Part[], + }, + ] + + const result = ProviderTransform.message(await MessageV2.toModelMessages(input, anthropicModel), anthropicModel, {}) + expect(result).toHaveLength(3) + expect(result[2].role).toBe("tool") + expect(result[2].content[0]).toMatchObject({ + type: "tool-result", + toolCallId: "call-anthropic-1", + toolName: "read", + output: { + type: "content", + value: [ + { type: "text", text: "Image read successfully" }, + { type: "media", mediaType: "image/jpeg", data: jpeg }, + ], + }, + }) + }) + test("omits provider metadata when assistant model differs", async () => { const userID = "m-user" const assistantID = "m-assistant" diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index 7456990ad0..42817d15df 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -394,6 +394,19 @@ describe("tool.read truncation", () => { }), ) + it.live("detects attachment media from file contents", () => + Effect.gen(function* () { + const dir = yield* tmpdirScoped() + const jpeg = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) + yield* put(path.join(dir, "image.bin"), jpeg) + + const result = yield* exec(dir, { filePath: path.join(dir, "image.bin") }) + expect(result.output).toBe("Image read successfully") + expect(result.attachments?.[0].mime).toBe("image/jpeg") + expect(result.attachments?.[0].url.startsWith("data:image/jpeg;base64,")).toBe(true) + }), + ) + it.live("large image files are properly attached without error", () => Effect.gen(function* () { const result = yield* exec(FIXTURES_DIR, { filePath: path.join(FIXTURES_DIR, "large-image.png") }) From c5c38cad9c444070fa3b18d569fa75eb2ab40407 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 18 Apr 2026 16:00:01 +0000 Subject: [PATCH 191/335] chore: generate --- packages/opencode/src/tool/read.ts | 6 +++++- packages/opencode/src/util/media.ts | 5 +---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 29d36692c6..c9b3048626 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -78,7 +78,11 @@ export const ReadTool = Tool.define( yield* lsp.touchFile(filepath, false).pipe(Effect.ignore, Effect.forkIn(scope)) }) - const readSample = Effect.fn("ReadTool.readSample")(function* (filepath: string, fileSize: number, sampleSize: number) { + const readSample = Effect.fn("ReadTool.readSample")(function* ( + filepath: string, + fileSize: number, + sampleSize: number, + ) { if (fileSize === 0) return new Uint8Array() return yield* Effect.scoped( diff --git a/packages/opencode/src/util/media.ts b/packages/opencode/src/util/media.ts index 0e98f53a52..566ac843a6 100644 --- a/packages/opencode/src/util/media.ts +++ b/packages/opencode/src/util/media.ts @@ -18,10 +18,7 @@ export function sniffAttachmentMime(bytes: Uint8Array, fallback: string) { if (startsWith(bytes, [0x47, 0x49, 0x46, 0x38])) return "image/gif" if (startsWith(bytes, [0x42, 0x4d])) return "image/bmp" if (startsWith(bytes, [0x25, 0x50, 0x44, 0x46, 0x2d])) return "application/pdf" - if ( - startsWith(bytes, [0x52, 0x49, 0x46, 0x46]) && - startsWith(bytes.subarray(8), [0x57, 0x45, 0x42, 0x50]) - ) { + if (startsWith(bytes, [0x52, 0x49, 0x46, 0x46]) && startsWith(bytes.subarray(8), [0x57, 0x45, 0x42, 0x50])) { return "image/webp" } From 5eaef6b758cbfb683deadb8d440a420c3c1ee1f8 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 18 Apr 2026 12:31:59 -0400 Subject: [PATCH 192/335] release: avoid package.json drift during publish --- packages/plugin/script/publish.ts | 36 ++++++++++++++++++------------- packages/sdk/js/script/publish.ts | 19 +++++++++------- script/publish.ts | 17 +++++++++++++++ 3 files changed, 49 insertions(+), 23 deletions(-) diff --git a/packages/plugin/script/publish.ts b/packages/plugin/script/publish.ts index de129918cd..fea8c7230c 100755 --- a/packages/plugin/script/publish.ts +++ b/packages/plugin/script/publish.ts @@ -11,22 +11,28 @@ async function published(name: string, version: string) { } await $`bun tsc` -const pkg = await import("../package.json").then( - (m) => m.default as { name: string; version: string; exports: Record }, -) -const original = JSON.parse(JSON.stringify(pkg)) +const originalText = await Bun.file("package.json").text() +const pkg = JSON.parse(originalText) as { + name: string + version: string + exports: Record +} if (await published(pkg.name, pkg.version)) { console.log(`already published ${pkg.name}@${pkg.version}`) - process.exit(0) -} -for (const [key, value] of Object.entries(pkg.exports)) { - const file = value.replace("./src/", "./dist/").replace(".ts", "") - // @ts-ignore - pkg.exports[key] = { - import: file + ".js", - types: file + ".d.ts", +} else { + for (const [key, value] of Object.entries(pkg.exports)) { + const file = value.replace("./src/", "./dist/").replace(".ts", "") + // @ts-ignore + pkg.exports[key] = { + import: file + ".js", + types: file + ".d.ts", + } + } + await Bun.write("package.json", JSON.stringify(pkg, null, 2)) + try { + await $`bun pm pack` + await $`npm publish *.tgz --tag ${Script.channel} --access public` + } finally { + await Bun.write("package.json", originalText) } } -await Bun.write("package.json", JSON.stringify(pkg, null, 2)) -await $`bun pm pack && npm publish *.tgz --tag ${Script.channel} --access public` -await Bun.write("package.json", JSON.stringify(original, null, 2)) diff --git a/packages/sdk/js/script/publish.ts b/packages/sdk/js/script/publish.ts index b5e1211fc4..29426a41b7 100755 --- a/packages/sdk/js/script/publish.ts +++ b/packages/sdk/js/script/publish.ts @@ -11,12 +11,12 @@ async function published(name: string, version: string) { return (await $`npm view ${name}@${version} version`.nothrow()).exitCode === 0 } -const pkg = (await import("../package.json").then((m) => m.default)) as { +const originalText = await Bun.file("package.json").text() +const pkg = JSON.parse(originalText) as { name: string version: string exports: Record } -const original = JSON.parse(JSON.stringify(pkg)) function transformExports(exports: Record) { return Object.fromEntries( Object.entries(exports).map(([key, value]) => { @@ -33,10 +33,13 @@ function transformExports(exports: Record) { } if (await published(pkg.name, pkg.version)) { console.log(`already published ${pkg.name}@${pkg.version}`) - process.exit(0) +} else { + pkg.exports = transformExports(pkg.exports) + await Bun.write("package.json", JSON.stringify(pkg, null, 2)) + try { + await $`bun pm pack` + await $`npm publish *.tgz --tag ${Script.channel} --access public` + } finally { + await Bun.write("package.json", originalText) + } } -pkg.exports = transformExports(pkg.exports) -await Bun.write("package.json", JSON.stringify(pkg, null, 2)) -await $`bun pm pack` -await $`npm publish *.tgz --tag ${Script.channel} --access public` -await Bun.write("package.json", JSON.stringify(original, null, 2)) diff --git a/script/publish.ts b/script/publish.ts index 6cd244e0e6..c8e39a4b3d 100755 --- a/script/publish.ts +++ b/script/publish.ts @@ -15,11 +15,23 @@ const pkgjsons = await Array.fromAsync( ).then((arr) => arr.filter((x) => !x.includes("node_modules") && !x.includes("dist"))) const extensionToml = fileURLToPath(new URL("../packages/extensions/zed/extension.toml", import.meta.url)) +const publishPackageJsons = ["packages/plugin/package.json", "packages/sdk/js/package.json"] async function hasChanges() { return (await $`git diff --quiet && git diff --cached --quiet`.nothrow()).exitCode !== 0 } +async function hasPublishPackageJsonChanges() { + if ((await $`git diff --quiet -- ${publishPackageJsons}`.nothrow()).exitCode !== 0) return true + return (await $`git diff --cached --quiet -- ${publishPackageJsons}`.nothrow()).exitCode !== 0 +} + +async function logPublishPackageJsonChanges() { + await $`git status --short -- ${publishPackageJsons}` + await $`git diff -- ${publishPackageJsons}` + await $`git diff --cached -- ${publishPackageJsons}` +} + async function releaseTagExists() { return (await $`git rev-parse -q --verify refs/tags/${tag}`.nothrow()).exitCode === 0 } @@ -76,6 +88,11 @@ if (Script.release) { if (Script.release && !Script.preview) { await $`git fetch origin` + if (await hasPublishPackageJsonChanges()) { + console.error("publish scripts left package.json changes before syncing dev") + await logPublishPackageJsonChanges() + throw new Error("packages/plugin/package.json or packages/sdk/js/package.json changed during publish") + } await $`git checkout -B dev origin/dev` await prepareReleaseFiles() if (await hasChanges()) { From a26d53151b11151a5580f11790b768ac334fa6a8 Mon Sep 17 00:00:00 2001 From: Dax Date: Sat, 18 Apr 2026 16:20:23 -0400 Subject: [PATCH 193/335] tui: allow full-session forks from the session dialog (#23339) --- .../session/dialog-fork-from-timeline.tsx | 25 +++++++++++++------ .../src/cli/cmd/tui/routes/session/index.tsx | 3 ++- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx index 8d1e4438c8..7414cefd3d 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx @@ -5,11 +5,11 @@ import type { TextPart } from "@opencode-ai/sdk/v2" import { Locale } from "@/util" import { useSDK } from "@tui/context/sdk" import { useRoute } from "@tui/context/route" -import { useDialog } from "../../ui/dialog" +import { useDialog, type DialogContext } from "../../ui/dialog" import type { PromptInfo } from "@tui/component/prompt/history" import { strip } from "@tui/component/prompt/part" -export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID: string) => void }) { +export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID?: string) => void }) { const sync = useSync() const dialog = useDialog() const sdk = useSDK() @@ -19,9 +19,21 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess dialog.setSize("large") }) - const options = createMemo((): DialogSelectOption[] => { + const options = createMemo((): DialogSelectOption[] => { const messages = sync.data.message[props.sessionID] ?? [] - const result = [] as DialogSelectOption[] + const fullSession = { + title: "Full session", + value: undefined, + onSelect: async (dialog: DialogContext) => { + const forked = await sdk.client.session.fork({ sessionID: props.sessionID }) + route.navigate({ + sessionID: forked.data!.id, + type: "session", + }) + dialog.clear() + }, + } satisfies DialogSelectOption + const result = [] as DialogSelectOption[] for (const message of messages) { if (message.role !== "user") continue const part = (sync.data.part[message.id] ?? []).find( @@ -57,9 +69,8 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess }, }) } - result.reverse() - return result + return [fullSession, ...result.reverse()] }) - return props.onMove(option.value)} title="Fork from message" options={options()} /> + return props.onMove(option.value)} title="Fork session" options={options()} /> } diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index ccca4d1eba..06be5dfbef 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -451,7 +451,7 @@ export function Session() { }, }, { - title: "Fork from message", + title: "Fork session", value: "session.fork", keybind: "session_fork", category: "Session", @@ -462,6 +462,7 @@ export function Session() { dialog.replace(() => ( { + if (!messageID) return const child = scroll.getChildren().find((child) => { return child.id === messageID }) From ce7923adafd03e9cea88f42b68a9adc6fece7156 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:00:46 -0500 Subject: [PATCH 194/335] chore: bump @ai-sdk/amazon-bedrock (#23341) --- bun.lock | 4 ++-- packages/opencode/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 3cb3cbea60..947e355d7f 100644 --- a/bun.lock +++ b/bun.lock @@ -322,7 +322,7 @@ "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.16.1", "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/amazon-bedrock": "4.0.95", + "@ai-sdk/amazon-bedrock": "4.0.96", "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/azure": "3.0.49", "@ai-sdk/cerebras": "2.0.41", @@ -740,7 +740,7 @@ "@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="], - "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.95", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-qJKWEy+cNx3bLSJi/XpIVhv0P8KO0JFB1SvEroNWN8gKm820SIglBmXS10DTeXJdM5PPbQX4i/wJj5BHEk2LRQ=="], + "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.96", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Mc4Ias2jRMD1jOB6xWtKNPdhECeuCZyIlbr9EAGfBnyBt++sS13ziZh9qv9TdyMCAZJ7xoQcpbchoRJcKwPdpA=="], "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 2acbc4fe84..40f25f8f7b 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -79,7 +79,7 @@ "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.16.1", "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/amazon-bedrock": "4.0.95", + "@ai-sdk/amazon-bedrock": "4.0.96", "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/azure": "3.0.49", "@ai-sdk/cerebras": "2.0.41", From e2e7a8d722cf4b0a387717e9eee6d2f8fd4a81d4 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:04:00 -0500 Subject: [PATCH 195/335] fix: ensure display: summarized is sent by default for bedrock (#23343) --- packages/opencode/src/provider/transform.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 1b6b0918b1..4ed43ce994 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -635,6 +635,9 @@ export function variants(model: Provider.Model): Record Date: Sat, 18 Apr 2026 21:32:47 +0000 Subject: [PATCH 196/335] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 01366c82dc..34b9095562 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-GjpBQhvGLTM6NWX29b/mS+KjrQPl0w9VjQHH5jaK9SM=", - "aarch64-linux": "sha256-F5h9p+iZ8CASdUYaYR7O22NwBRa/iT+ZinUxO8lbPTc=", - "aarch64-darwin": "sha256-jWo5yvCtjVKRf9i5XUcTTaLtj2+G6+T1Td2llO/cT5I=", - "x86_64-darwin": "sha256-LzV+5/8P2mkiFHmt+a8zDeJjRbU8z9nssSA4tzv1HxA=" + "x86_64-linux": "sha256-Spc0qzg4he0d0GwHcqj7uBmvK4DIF1tEbsZ6M+pxpWc=", + "aarch64-linux": "sha256-gwz/PKBbT+72hr7vUG28cdx4Z7/Sf06PNMr9JBjAYg0=", + "aarch64-darwin": "sha256-Lj8p9P/QzLqxiM1OVSwcbtTsms8AcW3A6H0575ERufw=", + "x86_64-darwin": "sha256-y0e+TnXj6wKDqSC5hQAWjpKadaFvL6GJ6Mba5anBM+Y=" } } From 9d012b062186ef9900cc2673b77c446d38ebd789 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 18 Apr 2026 16:26:58 -0400 Subject: [PATCH 197/335] zen: redeem credit --- packages/console/app/src/i18n/ar.ts | 7 + packages/console/app/src/i18n/br.ts | 7 + packages/console/app/src/i18n/da.ts | 7 + packages/console/app/src/i18n/de.ts | 7 + packages/console/app/src/i18n/en.ts | 7 + packages/console/app/src/i18n/es.ts | 7 + packages/console/app/src/i18n/fr.ts | 7 + packages/console/app/src/i18n/it.ts | 7 + packages/console/app/src/i18n/ja.ts | 7 + packages/console/app/src/i18n/ko.ts | 7 + packages/console/app/src/i18n/no.ts | 7 + packages/console/app/src/i18n/pl.ts | 7 + packages/console/app/src/i18n/ru.ts | 7 + packages/console/app/src/i18n/th.ts | 7 + packages/console/app/src/i18n/tr.ts | 7 + packages/console/app/src/i18n/zh.ts | 7 + packages/console/app/src/i18n/zht.ts | 7 + .../routes/workspace/[id]/billing/index.tsx | 2 + .../[id]/billing/redeem-section.module.css | 61 + .../workspace/[id]/billing/redeem-section.tsx | 71 + .../migration.sql | 6 + .../snapshot.json | 2670 +++++++++++++++++ packages/console/core/script/create-coupon.ts | 24 + packages/console/core/src/billing.ts | 33 +- .../console/core/src/schema/billing.sql.ts | 24 +- 25 files changed, 3007 insertions(+), 3 deletions(-) create mode 100644 packages/console/app/src/routes/workspace/[id]/billing/redeem-section.module.css create mode 100644 packages/console/app/src/routes/workspace/[id]/billing/redeem-section.tsx create mode 100644 packages/console/core/migrations/20260418195905_shocking_marvel_zombies/migration.sql create mode 100644 packages/console/core/migrations/20260418195905_shocking_marvel_zombies/snapshot.json create mode 100644 packages/console/core/script/create-coupon.ts diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index 11f76ca598..713703b256 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -558,6 +558,13 @@ export const dict = { "workspace.monthlyLimit.currentUsage.beforeMonth": "الاستخدام الحالي لـ", "workspace.monthlyLimit.currentUsage.beforeAmount": "هو $", + "workspace.redeem.title": "استرداد قسيمة", + "workspace.redeem.subtitle": "استرد رمز القسيمة للحصول على رصيد أو مزايا.", + "workspace.redeem.placeholder": "أدخل رمز القسيمة", + "workspace.redeem.redeem": "استرداد", + "workspace.redeem.redeeming": "جارٍ الاسترداد...", + "workspace.redeem.success": "تم استرداد القسيمة بنجاح.", + "workspace.reload.title": "إعادة الشحن التلقائي", "workspace.reload.disabled.before": "إعادة الشحن التلقائي", "workspace.reload.disabled.state": "معطّل", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index 79f64485a1..bace965696 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -567,6 +567,13 @@ export const dict = { "workspace.monthlyLimit.currentUsage.beforeMonth": "Uso atual para", "workspace.monthlyLimit.currentUsage.beforeAmount": "é $", + "workspace.redeem.title": "Resgatar Cupom", + "workspace.redeem.subtitle": "Resgate um código de cupom para receber créditos ou vantagens.", + "workspace.redeem.placeholder": "Digite o código do cupom", + "workspace.redeem.redeem": "Resgatar", + "workspace.redeem.redeeming": "Resgatando...", + "workspace.redeem.success": "Cupom resgatado com sucesso.", + "workspace.reload.title": "Recarga Automática", "workspace.reload.disabled.before": "A recarga automática está", "workspace.reload.disabled.state": "desativada", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index 97b148c066..c2d3bf3ba0 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -563,6 +563,13 @@ export const dict = { "workspace.monthlyLimit.currentUsage.beforeMonth": "Nuværende brug for", "workspace.monthlyLimit.currentUsage.beforeAmount": "er $", + "workspace.redeem.title": "Indløs kupon", + "workspace.redeem.subtitle": "Indløs en kuponkode for at få kreditter eller fordele.", + "workspace.redeem.placeholder": "Indtast kuponkode", + "workspace.redeem.redeem": "Indløs", + "workspace.redeem.redeeming": "Indløser...", + "workspace.redeem.success": "Kuponen blev indløst.", + "workspace.reload.title": "Automatisk genopfyldning", "workspace.reload.disabled.before": "Automatisk genopfyldning er", "workspace.reload.disabled.state": "deaktiveret", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index 1c5f631aea..e44335bab9 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -566,6 +566,13 @@ export const dict = { "workspace.monthlyLimit.currentUsage.beforeMonth": "Aktuelle Nutzung für", "workspace.monthlyLimit.currentUsage.beforeAmount": "ist $", + "workspace.redeem.title": "Gutschein einlösen", + "workspace.redeem.subtitle": "Löse einen Gutscheincode ein, um Guthaben oder Vorteile zu erhalten.", + "workspace.redeem.placeholder": "Gutscheincode eingeben", + "workspace.redeem.redeem": "Einlösen", + "workspace.redeem.redeeming": "Wird eingelöst...", + "workspace.redeem.success": "Gutschein erfolgreich eingelöst.", + "workspace.reload.title": "Auto-Reload", "workspace.reload.disabled.before": "Auto-Reload ist", "workspace.reload.disabled.state": "deaktiviert", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index d21d0dc9e0..a9c3ca3cc5 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -559,6 +559,13 @@ export const dict = { "workspace.monthlyLimit.currentUsage.beforeMonth": "Current usage for", "workspace.monthlyLimit.currentUsage.beforeAmount": "is $", + "workspace.redeem.title": "Redeem Coupon", + "workspace.redeem.subtitle": "Redeem a coupon code to claim credits or perks.", + "workspace.redeem.placeholder": "Enter coupon code", + "workspace.redeem.redeem": "Redeem", + "workspace.redeem.redeeming": "Redeeming...", + "workspace.redeem.success": "Coupon redeemed successfully.", + "workspace.reload.title": "Auto Reload", "workspace.reload.disabled.before": "Auto reload is", "workspace.reload.disabled.state": "disabled", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index d6449fd954..e2a0c271a5 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -567,6 +567,13 @@ export const dict = { "workspace.monthlyLimit.currentUsage.beforeMonth": "Uso actual para", "workspace.monthlyLimit.currentUsage.beforeAmount": "es $", + "workspace.redeem.title": "Canjear cupón", + "workspace.redeem.subtitle": "Canjea un código de cupón para obtener crédito o beneficios.", + "workspace.redeem.placeholder": "Introduce el código del cupón", + "workspace.redeem.redeem": "Canjear", + "workspace.redeem.redeeming": "Canjeando...", + "workspace.redeem.success": "Cupón canjeado correctamente.", + "workspace.reload.title": "Auto Recarga", "workspace.reload.disabled.before": "La auto recarga está", "workspace.reload.disabled.state": "deshabilitada", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index c54c431040..5a353e8735 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -569,6 +569,13 @@ export const dict = { "workspace.monthlyLimit.currentUsage.beforeMonth": "L'utilisation actuelle pour", "workspace.monthlyLimit.currentUsage.beforeAmount": "est de", + "workspace.redeem.title": "Utiliser un coupon", + "workspace.redeem.subtitle": "Utilisez un code promo pour obtenir du crédit ou des avantages.", + "workspace.redeem.placeholder": "Saisissez le code promo", + "workspace.redeem.redeem": "Utiliser", + "workspace.redeem.redeeming": "Utilisation...", + "workspace.redeem.success": "Coupon utilisé avec succès.", + "workspace.reload.title": "Rechargement automatique", "workspace.reload.disabled.before": "Le rechargement automatique est", "workspace.reload.disabled.state": "désactivé", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index aadea6fec0..b19bff9788 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -565,6 +565,13 @@ export const dict = { "workspace.monthlyLimit.currentUsage.beforeMonth": "Utilizzo attuale per", "workspace.monthlyLimit.currentUsage.beforeAmount": "è $", + "workspace.redeem.title": "Riscatta Coupon", + "workspace.redeem.subtitle": "Riscatta un codice coupon per ottenere credito o vantaggi.", + "workspace.redeem.placeholder": "Inserisci il codice coupon", + "workspace.redeem.redeem": "Riscatta", + "workspace.redeem.redeeming": "Riscatto in corso...", + "workspace.redeem.success": "Coupon riscattato con successo.", + "workspace.reload.title": "Ricarica Auto", "workspace.reload.disabled.before": "La ricarica auto è", "workspace.reload.disabled.state": "disabilitata", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index f3b4c083e9..1571345e5d 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -564,6 +564,13 @@ export const dict = { "workspace.monthlyLimit.currentUsage.beforeMonth": "現在の使用状況(", "workspace.monthlyLimit.currentUsage.beforeAmount": ")は $", + "workspace.redeem.title": "クーポンを利用", + "workspace.redeem.subtitle": "クーポンコードを利用して、クレジットや特典を受け取ります。", + "workspace.redeem.placeholder": "クーポンコードを入力", + "workspace.redeem.redeem": "利用する", + "workspace.redeem.redeeming": "利用中...", + "workspace.redeem.success": "クーポンを利用しました。", + "workspace.reload.title": "自動チャージ", "workspace.reload.disabled.before": "自動チャージは", "workspace.reload.disabled.state": "無効", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index e2320c359f..9ec9310314 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -558,6 +558,13 @@ export const dict = { "workspace.monthlyLimit.currentUsage.beforeMonth": "현재", "workspace.monthlyLimit.currentUsage.beforeAmount": "사용량: $", + "workspace.redeem.title": "쿠폰 사용", + "workspace.redeem.subtitle": "쿠폰 코드를 사용해 크레딧이나 혜택을 받으세요.", + "workspace.redeem.placeholder": "쿠폰 코드를 입력하세요", + "workspace.redeem.redeem": "사용", + "workspace.redeem.redeeming": "사용 중...", + "workspace.redeem.success": "쿠폰을 성공적으로 사용했습니다.", + "workspace.reload.title": "자동 충전", "workspace.reload.disabled.before": "자동 충전이", "workspace.reload.disabled.state": "비활성화", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index 717c2ad1ec..132b85a6e2 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -564,6 +564,13 @@ export const dict = { "workspace.monthlyLimit.currentUsage.beforeMonth": "Gjeldende forbruk for", "workspace.monthlyLimit.currentUsage.beforeAmount": "er $", + "workspace.redeem.title": "Løs inn kupong", + "workspace.redeem.subtitle": "Løs inn en kupongkode for å få kreditt eller fordeler.", + "workspace.redeem.placeholder": "Skriv inn kupongkode", + "workspace.redeem.redeem": "Løs inn", + "workspace.redeem.redeeming": "Løser inn...", + "workspace.redeem.success": "Kupongen ble løst inn.", + "workspace.reload.title": "Auto-påfyll", "workspace.reload.disabled.before": "Auto-påfyll er", "workspace.reload.disabled.state": "deaktivert", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index c9daeedee3..441dfc7bea 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -565,6 +565,13 @@ export const dict = { "workspace.monthlyLimit.currentUsage.beforeMonth": "Aktualne użycie za", "workspace.monthlyLimit.currentUsage.beforeAmount": "wynosi $", + "workspace.redeem.title": "Zrealizuj kupon", + "workspace.redeem.subtitle": "Zrealizuj kod kuponu, aby otrzymać środki lub korzyści.", + "workspace.redeem.placeholder": "Wpisz kod kuponu", + "workspace.redeem.redeem": "Zrealizuj", + "workspace.redeem.redeeming": "Realizowanie...", + "workspace.redeem.success": "Kupon został zrealizowany.", + "workspace.reload.title": "Automatyczne doładowanie", "workspace.reload.disabled.before": "Automatyczne doładowanie jest", "workspace.reload.disabled.state": "wyłączone", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index 01baa89852..ef7bcacd8b 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -571,6 +571,13 @@ export const dict = { "workspace.monthlyLimit.currentUsage.beforeMonth": "Текущее использование за", "workspace.monthlyLimit.currentUsage.beforeAmount": "составляет $", + "workspace.redeem.title": "Активировать купон", + "workspace.redeem.subtitle": "Активируйте код купона, чтобы получить кредит или бонусы.", + "workspace.redeem.placeholder": "Введите код купона", + "workspace.redeem.redeem": "Активировать", + "workspace.redeem.redeeming": "Активация...", + "workspace.redeem.success": "Купон успешно активирован.", + "workspace.reload.title": "Автопополнение", "workspace.reload.disabled.before": "Автопополнение", "workspace.reload.disabled.state": "отключено", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index 59c90ef65d..d7d862d948 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -560,6 +560,13 @@ export const dict = { "workspace.monthlyLimit.currentUsage.beforeMonth": "การใช้งานปัจจุบันสำหรับ", "workspace.monthlyLimit.currentUsage.beforeAmount": "คือ $", + "workspace.redeem.title": "แลกคูปอง", + "workspace.redeem.subtitle": "แลกรหัสคูปองเพื่อรับเครดิตหรือสิทธิพิเศษ", + "workspace.redeem.placeholder": "กรอกรหัสคูปอง", + "workspace.redeem.redeem": "แลก", + "workspace.redeem.redeeming": "กำลังแลก...", + "workspace.redeem.success": "แลกคูปองสำเร็จ", + "workspace.reload.title": "โหลดซ้ำอัตโนมัติ", "workspace.reload.disabled.before": "การโหลดซ้ำอัตโนมัติ", "workspace.reload.disabled.state": "ปิดใช้งานอยู่", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index 196bf9d376..13a074642d 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -567,6 +567,13 @@ export const dict = { "workspace.monthlyLimit.currentUsage.beforeMonth": "Şu anki kullanım", "workspace.monthlyLimit.currentUsage.beforeAmount": "$", + "workspace.redeem.title": "Kupon Kullan", + "workspace.redeem.subtitle": "Kredi veya avantajlardan yararlanmak için bir kupon kodu kullanın.", + "workspace.redeem.placeholder": "Kupon kodunu girin", + "workspace.redeem.redeem": "Kullan", + "workspace.redeem.redeeming": "Kullanılıyor...", + "workspace.redeem.success": "Kupon başarıyla kullanıldı.", + "workspace.reload.title": "Otomatik Yeniden Yükleme", "workspace.reload.disabled.before": "Otomatik yeniden yükleme:", "workspace.reload.disabled.state": "devre dışı", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index aaec74fbaa..c84ea5cc6b 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -542,6 +542,13 @@ export const dict = { "workspace.monthlyLimit.currentUsage.beforeMonth": "当前", "workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量为 $", + "workspace.redeem.title": "兑换优惠券", + "workspace.redeem.subtitle": "兑换优惠码以领取充值额度或权益。", + "workspace.redeem.placeholder": "输入优惠码", + "workspace.redeem.redeem": "兑换", + "workspace.redeem.redeeming": "兑换中...", + "workspace.redeem.success": "优惠券兑换成功。", + "workspace.reload.title": "自动充值", "workspace.reload.disabled.before": "自动充值已", "workspace.reload.disabled.state": "禁用", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index b3e0fb0778..6a70a81c71 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -542,6 +542,13 @@ export const dict = { "workspace.monthlyLimit.currentUsage.beforeMonth": "目前", "workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量為 $", + "workspace.redeem.title": "兌換優惠券", + "workspace.redeem.subtitle": "兌換優惠碼以領取儲值額度或權益。", + "workspace.redeem.placeholder": "輸入優惠碼", + "workspace.redeem.redeem": "兌換", + "workspace.redeem.redeeming": "兌換中...", + "workspace.redeem.success": "優惠券兌換成功。", + "workspace.reload.title": "自動儲值", "workspace.reload.disabled.before": "自動儲值已", "workspace.reload.disabled.state": "停用", diff --git a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx index 4a7dc24888..fb48485354 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx @@ -3,6 +3,7 @@ import { BillingSection } from "./billing-section" import { ReloadSection } from "./reload-section" import { PaymentSection } from "./payment-section" import { BlackSection } from "./black-section" +import { RedeemSection } from "./redeem-section" import { createMemo, Show } from "solid-js" import { createAsync, useParams } from "@solidjs/router" import { queryBillingInfo, querySessionInfo } from "../../common" @@ -26,6 +27,7 @@ export default function () { +
diff --git a/packages/console/app/src/routes/workspace/[id]/billing/redeem-section.module.css b/packages/console/app/src/routes/workspace/[id]/billing/redeem-section.module.css new file mode 100644 index 0000000000..42140e4e84 --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/billing/redeem-section.module.css @@ -0,0 +1,61 @@ +.root { + [data-slot="redeem-container"] { + display: flex; + flex-direction: column; + gap: var(--space-3); + min-width: 20rem; + width: fit-content; + + @media (max-width: 30rem) { + width: 100%; + } + } + + [data-slot="redeem-form"] { + display: flex; + flex-direction: column; + gap: var(--space-2); + + [data-slot="input-row"] { + display: flex; + gap: var(--space-2); + align-items: stretch; + + @media (max-width: 30rem) { + flex-direction: column; + } + } + + input { + flex: 1; + padding: var(--space-2) var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + background-color: var(--color-bg); + color: var(--color-text); + font-size: var(--font-size-sm); + font-family: var(--font-mono); + + &:focus { + outline: none; + border-color: var(--color-accent); + } + + &::placeholder { + color: var(--color-text-disabled); + } + } + + [data-slot="form-error"] { + color: var(--color-danger); + font-size: var(--font-size-sm); + line-height: 1.4; + } + + [data-slot="form-success"] { + color: var(--color-success, var(--color-accent)); + font-size: var(--font-size-sm); + line-height: 1.4; + } + } +} diff --git a/packages/console/app/src/routes/workspace/[id]/billing/redeem-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/redeem-section.tsx new file mode 100644 index 0000000000..872e8954be --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/billing/redeem-section.tsx @@ -0,0 +1,71 @@ +import { json, action, useParams, useSubmission } from "@solidjs/router" +import { Show } from "solid-js" +import { withActor } from "~/context/auth.withActor" +import { Billing } from "@opencode-ai/console-core/billing.js" +import { User } from "@opencode-ai/console-core/user.js" +import { Actor } from "@opencode-ai/console-core/actor.js" +import { CouponType } from "@opencode-ai/console-core/schema/billing.sql.js" +import styles from "./redeem-section.module.css" +import { queryBillingInfo } from "../../common" +import { useI18n } from "~/context/i18n" +import { formError, localizeError } from "~/lib/form-error" + +const redeem = action(async (form: FormData) => { + "use server" + const workspaceID = form.get("workspaceID") as string | null + if (!workspaceID) return { error: formError.workspaceRequired } + const code = (form.get("code") as string | null)?.trim().toUpperCase() + if (!code) return { error: "Coupon code is required." } + if (!(CouponType as readonly string[]).includes(code)) return { error: "Invalid coupon code." } + + return json( + await withActor(async () => { + const actor = Actor.assert("user") + const email = await User.getAuthEmail(actor.properties.userID) + if (!email) return { error: "No email on account." } + return Billing.redeemCoupon(email, code as (typeof CouponType)[number]) + .then(() => ({ error: undefined, data: true })) + .catch((e) => ({ error: e.message as string })) + }, workspaceID), + { revalidate: queryBillingInfo.key }, + ) +}, "billing.redeemCoupon") + +export function RedeemSection() { + const params = useParams() + const i18n = useI18n() + const submission = useSubmission(redeem) + + return ( +
+
+

{i18n.t("workspace.redeem.title")}

+

{i18n.t("workspace.redeem.subtitle")}

+
+
+
+
+ + +
+ + {(err: any) =>
{localizeError(i18n.t, err())}
} +
+ +
{i18n.t("workspace.redeem.success")}
+
+ +
+
+
+ ) +} diff --git a/packages/console/core/migrations/20260418195905_shocking_marvel_zombies/migration.sql b/packages/console/core/migrations/20260418195905_shocking_marvel_zombies/migration.sql new file mode 100644 index 0000000000..7b7f7a2ebc --- /dev/null +++ b/packages/console/core/migrations/20260418195905_shocking_marvel_zombies/migration.sql @@ -0,0 +1,6 @@ +CREATE TABLE `coupon` ( + `email` varchar(255), + `type` enum('BUILDATHON','GOFREEMONTH') NOT NULL, + `time_redeemed` timestamp(3), + CONSTRAINT PRIMARY KEY(`email`,`type`) +); diff --git a/packages/console/core/migrations/20260418195905_shocking_marvel_zombies/snapshot.json b/packages/console/core/migrations/20260418195905_shocking_marvel_zombies/snapshot.json new file mode 100644 index 0000000000..40e9d5e152 --- /dev/null +++ b/packages/console/core/migrations/20260418195905_shocking_marvel_zombies/snapshot.json @@ -0,0 +1,2670 @@ +{ + "version": "6", + "dialect": "mysql", + "id": "18b4281c-1609-47d8-9d51-0b08e3925f2b", + "prevIds": [ + "93c492af-c95b-4213-9fc2-38c3dd10374d" + ], + "ddl": [ + { + "name": "account", + "entityType": "tables" + }, + { + "name": "auth", + "entityType": "tables" + }, + { + "name": "benchmark", + "entityType": "tables" + }, + { + "name": "billing", + "entityType": "tables" + }, + { + "name": "coupon", + "entityType": "tables" + }, + { + "name": "lite", + "entityType": "tables" + }, + { + "name": "payment", + "entityType": "tables" + }, + { + "name": "subscription", + "entityType": "tables" + }, + { + "name": "usage", + "entityType": "tables" + }, + { + "name": "ip_rate_limit", + "entityType": "tables" + }, + { + "name": "ip", + "entityType": "tables" + }, + { + "name": "key_rate_limit", + "entityType": "tables" + }, + { + "name": "model_rate_limit", + "entityType": "tables" + }, + { + "name": "key", + "entityType": "tables" + }, + { + "name": "model", + "entityType": "tables" + }, + { + "name": "provider", + "entityType": "tables" + }, + { + "name": "user", + "entityType": "tables" + }, + { + "name": "workspace", + "entityType": "tables" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "account" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "auth" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "auth" + }, + { + "type": "enum('email','github','google')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subject", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "account_id", + "entityType": "columns", + "table": "auth" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "mediumtext", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "result", + "entityType": "columns", + "table": "benchmark" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "customer_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(32)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_type", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(4)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_method_last4", + "entityType": "columns", + "table": "billing" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "balance", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_limit", + "entityType": "columns", + "table": "billing" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_usage_updated", + "entityType": "columns", + "table": "billing" + }, + { + "type": "boolean", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_trigger", + "entityType": "columns", + "table": "billing" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_amount", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reload_error", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_reload_error", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_reload_locked_till", + "entityType": "columns", + "table": "billing" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(28)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "enum('20','100','200')", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "subscription_plan", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_subscription_booked", + "entityType": "columns", + "table": "billing" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_subscription_selected", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(28)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "lite_subscription_id", + "entityType": "columns", + "table": "billing" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "lite", + "entityType": "columns", + "table": "billing" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "coupon" + }, + { + "type": "enum('BUILDATHON','GOFREEMONTH')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "coupon" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_redeemed", + "entityType": "columns", + "table": "coupon" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "rolling_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "weekly_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_rolling_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_weekly_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_updated", + "entityType": "columns", + "table": "lite" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "customer_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "invoice_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "payment_id", + "entityType": "columns", + "table": "payment" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "amount", + "entityType": "columns", + "table": "payment" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_refunded", + "entityType": "columns", + "table": "payment" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "enrichment", + "entityType": "columns", + "table": "payment" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "rolling_usage", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "fixed_usage", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_rolling_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_fixed_updated", + "entityType": "columns", + "table": "subscription" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "usage" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "input_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "output_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "reasoning_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_read_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_write_5m_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cache_write_1h_tokens", + "entityType": "columns", + "table": "usage" + }, + { + "type": "bigint", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "usage" + }, + { + "type": "json", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "enrichment", + "entityType": "columns", + "table": "usage" + }, + { + "type": "varchar(45)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "ip", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "varchar(10)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "count", + "entityType": "columns", + "table": "ip_rate_limit" + }, + { + "type": "varchar(45)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "ip", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "ip" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "ip" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "usage", + "entityType": "columns", + "table": "ip" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key", + "entityType": "columns", + "table": "key_rate_limit" + }, + { + "type": "varchar(40)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "key_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "count", + "entityType": "columns", + "table": "key_rate_limit" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key", + "entityType": "columns", + "table": "model_rate_limit" + }, + { + "type": "varchar(40)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "interval", + "entityType": "columns", + "table": "model_rate_limit" + }, + { + "type": "int", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "count", + "entityType": "columns", + "table": "model_rate_limit" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "key", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "key" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "key" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "model" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "model" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "provider" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(64)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "provider" + }, + { + "type": "text", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "credentials", + "entityType": "columns", + "table": "provider" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "account_id", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_seen", + "entityType": "columns", + "table": "user" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "color", + "entityType": "columns", + "table": "user" + }, + { + "type": "enum('admin','member')", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "role", + "entityType": "columns", + "table": "user" + }, + { + "type": "int", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_limit", + "entityType": "columns", + "table": "user" + }, + { + "type": "bigint", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "monthly_usage", + "entityType": "columns", + "table": "user" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_monthly_usage_updated", + "entityType": "columns", + "table": "user" + }, + { + "type": "varchar(30)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "varchar(255)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "varchar(255)", + "notNull": true, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(now())", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": true, + "autoIncrement": false, + "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "timestamp(3)", + "notNull": false, + "autoIncrement": false, + "default": null, + "onUpdateNow": false, + "onUpdateNowFsp": null, + "charSet": null, + "collation": null, + "generated": null, + "name": "time_deleted", + "entityType": "columns", + "table": "workspace" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "auth", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "benchmark", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "billing", + "entityType": "pks" + }, + { + "columns": [ + "email", + "type" + ], + "name": "PRIMARY", + "table": "coupon", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "lite", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "payment", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "subscription", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "usage", + "entityType": "pks" + }, + { + "columns": [ + "ip", + "interval" + ], + "name": "PRIMARY", + "table": "ip_rate_limit", + "entityType": "pks" + }, + { + "columns": [ + "ip" + ], + "name": "PRIMARY", + "table": "ip", + "entityType": "pks" + }, + { + "columns": [ + "key", + "interval" + ], + "name": "PRIMARY", + "table": "key_rate_limit", + "entityType": "pks" + }, + { + "columns": [ + "key", + "interval" + ], + "name": "PRIMARY", + "table": "model_rate_limit", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "key", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "model", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "provider", + "entityType": "pks" + }, + { + "columns": [ + "workspace_id", + "id" + ], + "name": "PRIMARY", + "table": "user", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "name": "PRIMARY", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "provider", + "isExpression": false + }, + { + "value": "subject", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "provider", + "entityType": "indexes", + "table": "auth" + }, + { + "columns": [ + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "account_id", + "entityType": "indexes", + "table": "auth" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "time_created", + "entityType": "indexes", + "table": "benchmark" + }, + { + "columns": [ + { + "value": "customer_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_customer_id", + "entityType": "indexes", + "table": "billing" + }, + { + "columns": [ + { + "value": "subscription_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_subscription_id", + "entityType": "indexes", + "table": "billing" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_user_id", + "entityType": "indexes", + "table": "lite" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_user_id", + "entityType": "indexes", + "table": "subscription" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "usage_time_created", + "entityType": "indexes", + "table": "usage" + }, + { + "columns": [ + { + "value": "key", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_key", + "entityType": "indexes", + "table": "key" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "model", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "model_workspace_model", + "entityType": "indexes", + "table": "model" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "provider", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "workspace_provider", + "entityType": "indexes", + "table": "provider" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "user_account_id", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "email", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "user_email", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "account_id", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_account_id", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "email", + "isExpression": false + } + ], + "isUnique": false, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "global_email", + "entityType": "indexes", + "table": "user" + }, + { + "columns": [ + { + "value": "slug", + "isExpression": false + } + ], + "isUnique": true, + "using": null, + "algorithm": null, + "lock": null, + "nameExplicit": true, + "name": "slug", + "entityType": "indexes", + "table": "workspace" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/console/core/script/create-coupon.ts b/packages/console/core/script/create-coupon.ts new file mode 100644 index 0000000000..e133379058 --- /dev/null +++ b/packages/console/core/script/create-coupon.ts @@ -0,0 +1,24 @@ +import { Database } from "../src/drizzle/index.js" +import { CouponTable, CouponType } from "../src/schema/billing.sql.js" + +const email = process.argv[2] +const type = process.argv[3] as (typeof CouponType)[number] + +if (!email || !type) { + console.error(`Usage: bun create-coupon.ts <${CouponType.join("|")}>`) + process.exit(1) +} + +if (!(CouponType as readonly string[]).includes(type)) { + console.error(`Error: type must be one of ${CouponType.join(", ")}`) + process.exit(1) +} + +await Database.use((tx) => + tx.insert(CouponTable).values({ + email, + type, + }), +) + +console.log(`Created ${type} coupon for ${email}`) diff --git a/packages/console/core/src/billing.ts b/packages/console/core/src/billing.ts index 9de413e60b..d96b19ad79 100644 --- a/packages/console/core/src/billing.ts +++ b/packages/console/core/src/billing.ts @@ -1,6 +1,14 @@ import { Stripe } from "stripe" -import { Database, eq, sql } from "./drizzle" -import { BillingTable, LiteTable, PaymentTable, SubscriptionTable, UsageTable } from "./schema/billing.sql" +import { and, Database, eq, sql } from "./drizzle" +import { + BillingTable, + CouponTable, + CouponType, + LiteTable, + PaymentTable, + SubscriptionTable, + UsageTable, +} from "./schema/billing.sql" import { Actor } from "./actor" import { fn } from "./util/fn" import { z } from "zod" @@ -147,6 +155,27 @@ export namespace Billing { return amountInMicroCents } + export const redeemCoupon = async (email: string, type: (typeof CouponType)[number]) => { + const coupon = await Database.use((tx) => + tx + .select() + .from(CouponTable) + .where(and(eq(CouponTable.email, email), eq(CouponTable.type, type))) + .then((rows) => rows[0]), + ) + if (!coupon) throw new Error("Invalid coupon code") + if (coupon.timeRedeemed) throw new Error("Coupon already redeemed") + + if (type === "BUILDATHON") await grantCredit(Actor.workspace(), 500) + + await Database.use((tx) => + tx + .update(CouponTable) + .set({ timeRedeemed: sql`now()` }) + .where(and(eq(CouponTable.email, email), eq(CouponTable.type, type))), + ) + } + export const setMonthlyLimit = fn(z.number(), async (input) => { return await Database.use((tx) => tx diff --git a/packages/console/core/src/schema/billing.sql.ts b/packages/console/core/src/schema/billing.sql.ts index b06ca8966d..f8dcbd2b1b 100644 --- a/packages/console/core/src/schema/billing.sql.ts +++ b/packages/console/core/src/schema/billing.sql.ts @@ -1,4 +1,15 @@ -import { bigint, boolean, index, int, json, mysqlEnum, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core" +import { + bigint, + boolean, + index, + int, + json, + mysqlEnum, + mysqlTable, + primaryKey, + uniqueIndex, + varchar, +} from "drizzle-orm/mysql-core" import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types" import { workspaceIndexes } from "./workspace.sql" @@ -121,3 +132,14 @@ export const UsageTable = mysqlTable( }, (table) => [...workspaceIndexes(table), index("usage_time_created").on(table.workspaceID, table.timeCreated)], ) + +export const CouponType = ["BUILDATHON", "GOFREEMONTH"] as const +export const CouponTable = mysqlTable( + "coupon", + { + email: varchar("email", { length: 255 }), + type: mysqlEnum("type", CouponType).notNull(), + timeRedeemed: utc("time_redeemed"), + }, + (table) => [primaryKey({ columns: [table.email, table.type] })], +) From 54b3b3fe05fd66f8f3b479ec438b53d38dbc6d0d Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 18 Apr 2026 17:33:26 -0400 Subject: [PATCH 198/335] zen: redeem go --- infra/console.ts | 1 - .../console/app/src/routes/stripe/webhook.ts | 7 ++++++ .../routes/workspace/[id]/billing/index.tsx | 2 +- packages/console/core/src/billing.ts | 23 +++++++++++++++---- packages/console/core/src/lite.ts | 8 ++----- packages/console/core/sst-env.d.ts | 4 ---- packages/console/function/sst-env.d.ts | 4 ---- packages/console/resource/sst-env.d.ts | 4 ---- packages/enterprise/sst-env.d.ts | 4 ---- packages/function/sst-env.d.ts | 4 ---- sst-env.d.ts | 4 ---- 11 files changed, 29 insertions(+), 36 deletions(-) diff --git a/infra/console.ts b/infra/console.ts index 8925f37d5a..f1f5692b7a 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -236,7 +236,6 @@ new sst.cloudflare.x.SolidStart("Console", { SALESFORCE_INSTANCE_URL, ZEN_BLACK_PRICE, ZEN_LITE_PRICE, - new sst.Secret("ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES"), new sst.Secret("ZEN_LIMITS"), new sst.Secret("ZEN_SESSION_SECRET"), ...ZEN_MODELS, diff --git a/packages/console/app/src/routes/stripe/webhook.ts b/packages/console/app/src/routes/stripe/webhook.ts index 0d8cf61cfa..c28d9ebbb9 100644 --- a/packages/console/app/src/routes/stripe/webhook.ts +++ b/packages/console/app/src/routes/stripe/webhook.ts @@ -9,6 +9,7 @@ import { Actor } from "@opencode-ai/console-core/actor.js" import { Resource } from "@opencode-ai/console-resource" import { LiteData } from "@opencode-ai/console-core/lite.js" import { BlackData } from "@opencode-ai/console-core/black.js" +import { User } from "@opencode-ai/console-core/user.js" export async function POST(input: APIEvent) { const body = await Billing.stripe().webhooks.constructEventAsync( @@ -109,6 +110,8 @@ export async function POST(input: APIEvent) { if (type === "lite") { const workspaceID = body.data.object.metadata?.workspaceID const userID = body.data.object.metadata?.userID + const userEmail = body.data.object.metadata?.userEmail + const coupon = body.data.object.metadata?.coupon const customerID = body.data.object.customer as string const invoiceID = body.data.object.latest_invoice as string const subscriptionID = body.data.object.id as string @@ -156,6 +159,10 @@ export async function POST(input: APIEvent) { id: Identifier.create("lite"), userID: userID, }) + + if (userEmail && coupon === LiteData.firstMonth100Coupon) { + await Billing.redeemCoupon(userEmail, "GOFREEMONTH") + } }) }) } diff --git a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx index fb48485354..11185436be 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx @@ -25,9 +25,9 @@ export default function () { + -
diff --git a/packages/console/core/src/billing.ts b/packages/console/core/src/billing.ts index d96b19ad79..a3252f1d78 100644 --- a/packages/console/core/src/billing.ts +++ b/packages/console/core/src/billing.ts @@ -1,5 +1,5 @@ import { Stripe } from "stripe" -import { and, Database, eq, sql } from "./drizzle" +import { and, Database, eq, isNull, sql } from "./drizzle" import { BillingTable, CouponTable, @@ -176,6 +176,16 @@ export namespace Billing { ) } + export const hasCoupon = async (email: string, type: (typeof CouponType)[number]) => { + return await Database.use((tx) => + tx + .select() + .from(CouponTable) + .where(and(eq(CouponTable.email, email), eq(CouponTable.type, type), isNull(CouponTable.timeRedeemed))) + .then((rows) => rows.length > 0), + ) + } + export const setMonthlyLimit = fn(z.number(), async (input) => { return await Database.use((tx) => tx @@ -274,16 +284,19 @@ export namespace Billing { const user = Actor.assert("user") const { successUrl, cancelUrl, method } = input - const email = await User.getAuthEmail(user.properties.userID) + const email = (await User.getAuthEmail(user.properties.userID))! const billing = await Billing.get() if (billing.subscriptionID) throw new Error("Already subscribed to Black") if (billing.liteSubscriptionID) throw new Error("Already subscribed to Lite") + const coupon = (await Billing.hasCoupon(email, "GOFREEMONTH")) + ? LiteData.firstMonth100Coupon + : LiteData.firstMonth50Coupon const createSession = () => Billing.stripe().checkout.sessions.create({ mode: "subscription", - discounts: [{ coupon: LiteData.firstMonthCoupon(email!) }], + discounts: [{ coupon }], ...(billing.customerID ? { customer: billing.customerID, @@ -293,7 +306,7 @@ export namespace Billing { }, } : { - customer_email: email!, + customer_email: email, }), ...(() => { if (method === "alipay") { @@ -341,6 +354,8 @@ export namespace Billing { metadata: { workspaceID: Actor.workspace(), userID: user.properties.userID, + userEmail: email, + coupon, type: "lite", }, }, diff --git a/packages/console/core/src/lite.ts b/packages/console/core/src/lite.ts index 3343192c19..c049776643 100644 --- a/packages/console/core/src/lite.ts +++ b/packages/console/core/src/lite.ts @@ -11,11 +11,7 @@ export namespace LiteData { export const productID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.product) export const priceID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.price) export const priceInr = fn(z.void(), () => Resource.ZEN_LITE_PRICE.priceInr) - export const firstMonthCoupon = fn(z.string(), (email) => { - const invitees = Resource.ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES.value.split(",") - return invitees.includes(email) - ? Resource.ZEN_LITE_PRICE.firstMonth100Coupon - : Resource.ZEN_LITE_PRICE.firstMonth50Coupon - }) + export const firstMonth100Coupon = Resource.ZEN_LITE_PRICE.firstMonth100Coupon + export const firstMonth50Coupon = Resource.ZEN_LITE_PRICE.firstMonth50Coupon export const planName = fn(z.void(), () => "lite") } diff --git a/packages/console/core/sst-env.d.ts b/packages/console/core/sst-env.d.ts index b77ee3c5bf..bfba1b8f2e 100644 --- a/packages/console/core/sst-env.d.ts +++ b/packages/console/core/sst-env.d.ts @@ -142,10 +142,6 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } - "ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": { - "type": "sst.sst.Secret" - "value": string - } "ZEN_LITE_PRICE": { "firstMonth100Coupon": string "firstMonth50Coupon": string diff --git a/packages/console/function/sst-env.d.ts b/packages/console/function/sst-env.d.ts index b77ee3c5bf..bfba1b8f2e 100644 --- a/packages/console/function/sst-env.d.ts +++ b/packages/console/function/sst-env.d.ts @@ -142,10 +142,6 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } - "ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": { - "type": "sst.sst.Secret" - "value": string - } "ZEN_LITE_PRICE": { "firstMonth100Coupon": string "firstMonth50Coupon": string diff --git a/packages/console/resource/sst-env.d.ts b/packages/console/resource/sst-env.d.ts index b77ee3c5bf..bfba1b8f2e 100644 --- a/packages/console/resource/sst-env.d.ts +++ b/packages/console/resource/sst-env.d.ts @@ -142,10 +142,6 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } - "ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": { - "type": "sst.sst.Secret" - "value": string - } "ZEN_LITE_PRICE": { "firstMonth100Coupon": string "firstMonth50Coupon": string diff --git a/packages/enterprise/sst-env.d.ts b/packages/enterprise/sst-env.d.ts index b77ee3c5bf..bfba1b8f2e 100644 --- a/packages/enterprise/sst-env.d.ts +++ b/packages/enterprise/sst-env.d.ts @@ -142,10 +142,6 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } - "ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": { - "type": "sst.sst.Secret" - "value": string - } "ZEN_LITE_PRICE": { "firstMonth100Coupon": string "firstMonth50Coupon": string diff --git a/packages/function/sst-env.d.ts b/packages/function/sst-env.d.ts index b77ee3c5bf..bfba1b8f2e 100644 --- a/packages/function/sst-env.d.ts +++ b/packages/function/sst-env.d.ts @@ -142,10 +142,6 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } - "ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": { - "type": "sst.sst.Secret" - "value": string - } "ZEN_LITE_PRICE": { "firstMonth100Coupon": string "firstMonth50Coupon": string diff --git a/sst-env.d.ts b/sst-env.d.ts index 2a40a9f3c9..e0c3665a96 100644 --- a/sst-env.d.ts +++ b/sst-env.d.ts @@ -168,10 +168,6 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } - "ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": { - "type": "sst.sst.Secret" - "value": string - } "ZEN_LITE_PRICE": { "firstMonth100Coupon": string "firstMonth50Coupon": string From 7e971d8302c3d2c0020bd81e94d3ca039b4fd9a9 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 18 Apr 2026 21:37:45 +0000 Subject: [PATCH 199/335] chore: generate --- .../snapshot.json | 91 ++++--------------- 1 file changed, 20 insertions(+), 71 deletions(-) diff --git a/packages/console/core/migrations/20260418195905_shocking_marvel_zombies/snapshot.json b/packages/console/core/migrations/20260418195905_shocking_marvel_zombies/snapshot.json index 40e9d5e152..44ccb08cc8 100644 --- a/packages/console/core/migrations/20260418195905_shocking_marvel_zombies/snapshot.json +++ b/packages/console/core/migrations/20260418195905_shocking_marvel_zombies/snapshot.json @@ -2,9 +2,7 @@ "version": "6", "dialect": "mysql", "id": "18b4281c-1609-47d8-9d51-0b08e3925f2b", - "prevIds": [ - "93c492af-c95b-4213-9fc2-38c3dd10374d" - ], + "prevIds": ["93c492af-c95b-4213-9fc2-38c3dd10374d"], "ddl": [ { "name": "account", @@ -2221,158 +2219,109 @@ "table": "workspace" }, { - "columns": [ - "id" - ], + "columns": ["id"], "name": "PRIMARY", "table": "account", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "name": "PRIMARY", "table": "auth", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "name": "PRIMARY", "table": "benchmark", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "billing", "entityType": "pks" }, { - "columns": [ - "email", - "type" - ], + "columns": ["email", "type"], "name": "PRIMARY", "table": "coupon", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "lite", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "payment", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "subscription", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "usage", "entityType": "pks" }, { - "columns": [ - "ip", - "interval" - ], + "columns": ["ip", "interval"], "name": "PRIMARY", "table": "ip_rate_limit", "entityType": "pks" }, { - "columns": [ - "ip" - ], + "columns": ["ip"], "name": "PRIMARY", "table": "ip", "entityType": "pks" }, { - "columns": [ - "key", - "interval" - ], + "columns": ["key", "interval"], "name": "PRIMARY", "table": "key_rate_limit", "entityType": "pks" }, { - "columns": [ - "key", - "interval" - ], + "columns": ["key", "interval"], "name": "PRIMARY", "table": "model_rate_limit", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "key", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "model", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "provider", "entityType": "pks" }, { - "columns": [ - "workspace_id", - "id" - ], + "columns": ["workspace_id", "id"], "name": "PRIMARY", "table": "user", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "name": "PRIMARY", "table": "workspace", "entityType": "pks" @@ -2667,4 +2616,4 @@ } ], "renames": [] -} \ No newline at end of file +} From 1d54b0e54065e3ad09093cff658447d0e2ffde3b Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Sat, 18 Apr 2026 18:30:28 -0400 Subject: [PATCH 200/335] Stefan/enterprise forms waitlist (#23158) Co-authored-by: Ryan Vogel --- .../console/app/src/routes/api/enterprise.ts | 71 ++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/packages/console/app/src/routes/api/enterprise.ts b/packages/console/app/src/routes/api/enterprise.ts index 1bc4d0eb29..6560a93610 100644 --- a/packages/console/app/src/routes/api/enterprise.ts +++ b/packages/console/app/src/routes/api/enterprise.ts @@ -1,5 +1,6 @@ import type { APIEvent } from "@solidjs/start/server" import { AWS } from "@opencode-ai/console-core/aws.js" +import { Resource } from "@opencode-ai/console-resource" import { i18n } from "~/i18n" import { localeFromRequest } from "~/lib/language" import { createLead } from "~/lib/salesforce" @@ -14,6 +15,64 @@ interface EnterpriseFormData { message: string } +const EMAIL_OCTOPUS_LIST_ID = "1b381e5e-39bd-11f1-ba4a-cdd4791f0c43" + +function splitFullName(fullName: string) { + const parts = fullName + .trim() + .split(/\s+/) + .filter((p) => p.length > 0) + if (parts.length === 0) return { firstName: "", lastName: "" } + if (parts.length === 1) return { firstName: parts[0], lastName: "" } + return { firstName: parts[0], lastName: parts.slice(1).join(" ") } +} + +function getEmailOctopusApiKey() { + if (process.env.EMAILOCTOPUS_API_KEY) return process.env.EMAILOCTOPUS_API_KEY + try { + return Resource.EMAILOCTOPUS_API_KEY.value + } catch { + return + } +} + +function subscribe(email: string, fullName: string) { + const apiKey = getEmailOctopusApiKey() + if (!apiKey) { + console.warn("Skipping EmailOctopus subscribe: missing API key") + return Promise.resolve(false) + } + + const name = splitFullName(fullName) + const fields: Record = {} + if (name.firstName) fields.FirstName = name.firstName + if (name.lastName) fields.LastName = name.lastName + + const payload: { email_address: string; fields?: Record } = { email_address: email } + if (Object.keys(fields).length) payload.fields = fields + + return fetch(`https://api.emailoctopus.com/lists/${EMAIL_OCTOPUS_LIST_ID}/contacts`, { + method: "PUT", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }).then( + (res) => { + if (!res.ok) { + console.error("EmailOctopus subscribe failed:", res.status, res.statusText) + return false + } + return true + }, + (err) => { + console.error("Failed to subscribe enterprise email:", err) + return false + }, + ) +} + export async function POST(event: APIEvent) { const dict = i18n(localeFromRequest(event.request)) try { @@ -41,7 +100,7 @@ ${body.role}
${body.company ? `${body.company}
` : ""}${body.email}
${body.phone ? `${body.phone}
` : ""}`.trim() - const [lead, mail] = await Promise.all([ + const [lead, mail, octopus] = await Promise.all([ createLead({ name: body.name, role: body.role, @@ -49,6 +108,9 @@ ${body.phone ? `${body.phone}
` : ""}`.trim() email: body.email, phone: body.phone, message: body.message, + }).catch((err) => { + console.error("Failed to create Salesforce lead:", err) + return false }), AWS.sendEmail({ to: "contact@anoma.ly", @@ -62,9 +124,14 @@ ${body.phone ? `${body.phone}
` : ""}`.trim() return false }, ), + subscribe(body.email, body.name), ]) - if (!lead && !mail) { + if (!lead && !mail && !octopus) { + if (import.meta.env.DEV) { + console.warn("Enterprise inquiry accepted in dev mode without integrations", { email: body.email }) + return Response.json({ success: true, message: dict["enterprise.form.success.submitted"] }, { status: 200 }) + } console.error("Enterprise inquiry delivery failed", { email: body.email }) return Response.json({ error: dict["enterprise.form.error.internalServer"] }, { status: 500 }) } From 78ca49a1bcdfe42482cb8e902d0e9c3248a50d7a Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 18 Apr 2026 17:46:15 -0500 Subject: [PATCH 201/335] test: fix bedrock test (#23351) --- packages/opencode/test/provider/transform.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index e195d9b177..7a7631710d 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -2804,12 +2804,14 @@ describe("ProviderTransform.variants", () => { reasoningConfig: { type: "adaptive", maxReasoningEffort: "xhigh", + display: "summarized", }, }) expect(result.max).toEqual({ reasoningConfig: { type: "adaptive", maxReasoningEffort: "max", + display: "summarized", }, }) }) From 940f971ca0368fadd03d654ccab79594672f4c48 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 18 Apr 2026 18:55:15 -0400 Subject: [PATCH 202/335] ci: fix --- script/publish.ts | 94 ++++++++++++++--------------------------------- 1 file changed, 28 insertions(+), 66 deletions(-) mode change 100755 => 100644 script/publish.ts diff --git a/script/publish.ts b/script/publish.ts old mode 100755 new mode 100644 index c8e39a4b3d..d1141c8f7e --- a/script/publish.ts +++ b/script/publish.ts @@ -6,6 +6,9 @@ import { fileURLToPath } from "url" console.log("=== publishing ===\n") +const dir = fileURLToPath(new URL("..", import.meta.url)) +process.chdir(dir) + const tag = `v${Script.version}` const pkgjsons = await Array.fromAsync( @@ -14,64 +17,29 @@ const pkgjsons = await Array.fromAsync( }), ).then((arr) => arr.filter((x) => !x.includes("node_modules") && !x.includes("dist"))) -const extensionToml = fileURLToPath(new URL("../packages/extensions/zed/extension.toml", import.meta.url)) -const publishPackageJsons = ["packages/plugin/package.json", "packages/sdk/js/package.json"] - -async function hasChanges() { - return (await $`git diff --quiet && git diff --cached --quiet`.nothrow()).exitCode !== 0 -} - -async function hasPublishPackageJsonChanges() { - if ((await $`git diff --quiet -- ${publishPackageJsons}`.nothrow()).exitCode !== 0) return true - return (await $`git diff --cached --quiet -- ${publishPackageJsons}`.nothrow()).exitCode !== 0 -} - -async function logPublishPackageJsonChanges() { - await $`git status --short -- ${publishPackageJsons}` - await $`git diff -- ${publishPackageJsons}` - await $`git diff --cached -- ${publishPackageJsons}` -} - -async function releaseTagExists() { - return (await $`git rev-parse -q --verify refs/tags/${tag}`.nothrow()).exitCode === 0 -} - -async function prepareReleaseFiles() { - for (const file of pkgjsons) { - let pkg = await Bun.file(file).text() - pkg = pkg.replaceAll(/"version": "[^"]+"/g, `"version": "${Script.version}"`) - console.log("updated:", file) - await Bun.file(file).write(pkg) - } - - let toml = await Bun.file(extensionToml).text() - toml = toml.replace(/^version = "[^"]+"/m, `version = "${Script.version}"`) - toml = toml.replaceAll(/releases\/download\/v[^/]+\//g, `releases/download/v${Script.version}/`) - console.log("updated:", extensionToml) - await Bun.file(extensionToml).write(toml) - - await $`bun install` - await $`./packages/sdk/js/script/build.ts` -} - if (Script.release && !Script.preview) { await $`git fetch origin --tags` await $`git switch --detach` } -await prepareReleaseFiles() - -if (Script.release && !Script.preview) { - if (await releaseTagExists()) { - console.log(`release tag ${tag} already exists, skipping tag creation`) - } else { - await $`git commit -am "release: ${tag}"` - await $`git tag ${tag}` - await $`git push origin refs/tags/${tag} --no-verify` - await new Promise((resolve) => setTimeout(resolve, 5_000)) - } +for (const file of pkgjsons) { + let pkg = await Bun.file(file).text() + pkg = pkg.replaceAll(/"version": "[^"]+"/g, `"version": "${Script.version}"`) + console.log("updated:", file) + await Bun.file(file).write(pkg) } +const extensionToml = fileURLToPath(new URL("../packages/extensions/zed/extension.toml", import.meta.url)) +let toml = await Bun.file(extensionToml).text() +toml = toml.replace(/^version = "[^"]+"/m, `version = "${Script.version}"`) +toml = toml.replaceAll(/releases\/download\/v[^/]+\//g, `releases/download/v${Script.version}/`) +console.log("updated:", extensionToml) +await Bun.file(extensionToml).write(toml) + +await $`bun install` +await import(`../packages/sdk/js/script/build.ts`) +process.chdir(dir) + console.log("\n=== cli ===\n") await import(`../packages/opencode/script/publish.ts`) @@ -87,25 +55,19 @@ if (Script.release) { } if (Script.release && !Script.preview) { + process.chdir(dir) + await $`git commit -am "release: ${tag}"` + const releaseCommit = (await $`git rev-parse HEAD`.text()).trim() + await $`git tag -d ${tag}`.nothrow() + await $`git tag ${tag}` + await $`git push origin refs/tags/${tag} --force-with-lease --no-verify` + await new Promise((resolve) => setTimeout(resolve, 5_000)) await $`git fetch origin` - if (await hasPublishPackageJsonChanges()) { - console.error("publish scripts left package.json changes before syncing dev") - await logPublishPackageJsonChanges() - throw new Error("packages/plugin/package.json or packages/sdk/js/package.json changed during publish") - } await $`git checkout -B dev origin/dev` - await prepareReleaseFiles() - if (await hasChanges()) { - await $`git commit -am "sync release versions for v${Script.version}"` - await $`git push origin HEAD:dev --no-verify` - } else { - console.log(`dev already synced for ${tag}`) - } + await $`git cherry-pick ${releaseCommit}` + await $`git push origin HEAD:dev --no-verify` } if (Script.release) { await $`gh release edit ${tag} --draft=false --repo ${process.env.GH_REPO}` } - -const dir = fileURLToPath(new URL("..", import.meta.url)) -process.chdir(dir) From cfbbae73236913794c2c63a469849cd6c88470a9 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 18 Apr 2026 18:59:31 -0400 Subject: [PATCH 203/335] ci --- script/publish.ts | 53 ++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/script/publish.ts b/script/publish.ts index d1141c8f7e..4d90bd9687 100644 --- a/script/publish.ts +++ b/script/publish.ts @@ -8,7 +8,6 @@ console.log("=== publishing ===\n") const dir = fileURLToPath(new URL("..", import.meta.url)) process.chdir(dir) - const tag = `v${Script.version}` const pkgjsons = await Array.fromAsync( @@ -17,54 +16,56 @@ const pkgjsons = await Array.fromAsync( }), ).then((arr) => arr.filter((x) => !x.includes("node_modules") && !x.includes("dist"))) +async function prepareReleaseFiles() { + for (const file of pkgjsons) { + let pkg = await Bun.file(file).text() + pkg = pkg.replaceAll(/"version": "[^"]+"/g, `"version": "${Script.version}"`) + console.log("updated:", file) + await Bun.file(file).write(pkg) + } + + const extensionToml = fileURLToPath(new URL("../packages/extensions/zed/extension.toml", import.meta.url)) + let toml = await Bun.file(extensionToml).text() + toml = toml.replace(/^version = "[^"]+"/m, `version = "${Script.version}"`) + toml = toml.replaceAll(/releases\/download\/v[^/]+\//g, `releases/download/v${Script.version}/`) + console.log("updated:", extensionToml) + await Bun.file(extensionToml).write(toml) + + await $`bun install` + await $`./packages/sdk/js/script/build.ts` +} + if (Script.release && !Script.preview) { await $`git fetch origin --tags` await $`git switch --detach` } -for (const file of pkgjsons) { - let pkg = await Bun.file(file).text() - pkg = pkg.replaceAll(/"version": "[^"]+"/g, `"version": "${Script.version}"`) - console.log("updated:", file) - await Bun.file(file).write(pkg) -} - -const extensionToml = fileURLToPath(new URL("../packages/extensions/zed/extension.toml", import.meta.url)) -let toml = await Bun.file(extensionToml).text() -toml = toml.replace(/^version = "[^"]+"/m, `version = "${Script.version}"`) -toml = toml.replaceAll(/releases\/download\/v[^/]+\//g, `releases/download/v${Script.version}/`) -console.log("updated:", extensionToml) -await Bun.file(extensionToml).write(toml) - -await $`bun install` -await import(`../packages/sdk/js/script/build.ts`) -process.chdir(dir) +await prepareReleaseFiles() console.log("\n=== cli ===\n") -await import(`../packages/opencode/script/publish.ts`) +await $`bun ./packages/opencode/script/publish.ts` console.log("\n=== sdk ===\n") -await import(`../packages/sdk/js/script/publish.ts`) +await $`bun ./packages/sdk/js/script/publish.ts` console.log("\n=== plugin ===\n") -await import(`../packages/plugin/script/publish.ts`) +await $`bun ./packages/plugin/script/publish.ts` if (Script.release) { - await import(`../packages/desktop/scripts/finalize-latest-json.ts`) - await import(`../packages/desktop-electron/scripts/finalize-latest-yml.ts`) + await $`bun ./packages/desktop/scripts/finalize-latest-json.ts` + await $`bun ./packages/desktop-electron/scripts/finalize-latest-yml.ts` } if (Script.release && !Script.preview) { - process.chdir(dir) await $`git commit -am "release: ${tag}"` - const releaseCommit = (await $`git rev-parse HEAD`.text()).trim() await $`git tag -d ${tag}`.nothrow() await $`git tag ${tag}` await $`git push origin refs/tags/${tag} --force-with-lease --no-verify` await new Promise((resolve) => setTimeout(resolve, 5_000)) await $`git fetch origin` await $`git checkout -B dev origin/dev` - await $`git cherry-pick ${releaseCommit}` + await prepareReleaseFiles() + await $`git commit -am "sync release versions for ${tag}"` await $`git push origin HEAD:dev --no-verify` } From ad0545335a9ac5c371f4bd51674cd6da2414e2e9 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 18 Apr 2026 19:29:21 -0400 Subject: [PATCH 204/335] ci --- script/publish.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 script/publish.ts diff --git a/script/publish.ts b/script/publish.ts old mode 100644 new mode 100755 From f27eb8f09ef0d9071ebb4d1b11625b27f49a8939 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 18 Apr 2026 20:02:24 -0400 Subject: [PATCH 205/335] fix plugins reinstalling too often --- packages/opencode/src/npm/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/opencode/src/npm/index.ts b/packages/opencode/src/npm/index.ts index 0760e768b9..477e99e06a 100644 --- a/packages/opencode/src/npm/index.ts +++ b/packages/opencode/src/npm/index.ts @@ -1,6 +1,7 @@ export * as Npm from "." import path from "path" +import npa from "npm-package-arg" import semver from "semver" import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect" import { NodeFileSystem } from "@effect/platform-node" @@ -135,6 +136,17 @@ export const layer = Layer.effect( const add = Effect.fn("Npm.add")(function* (pkg: string) { const dir = directory(pkg) + const name = (() => { + try { + return npa(pkg).name ?? pkg + } catch { + return pkg + } + })() + + if (yield* afs.existsSafe(dir)) { + return resolveEntryPoint(name, path.join(dir, "node_modules", name)) + } const tree = yield* reify({ dir, add: [pkg] }) const first = tree.edgesOut.values().next().value?.to From d1835686440d60686b7b9f66e160d47e747266da Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 18 Apr 2026 22:32:53 -0400 Subject: [PATCH 206/335] core: ensure executable permissions are set before Docker builds Fixes an issue where GitHub artifact downloads could strip executable bits from binaries, causing Docker builds to fail when using unpacked dist files directly rather than published tarballs. The chmod now runs before the publish check to guarantee binaries are executable. --- packages/opencode/script/publish.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts index b444106a1b..7557c475f7 100755 --- a/packages/opencode/script/publish.ts +++ b/packages/opencode/script/publish.ts @@ -12,11 +12,13 @@ async function published(name: string, version: string) { } async function publish(dir: string, name: string, version: string) { + // GitHub artifact downloads can drop the executable bit, and Docker uses the + // unpacked dist binaries directly rather than the published tarball. + if (process.platform !== "win32") await $`chmod -R 755 .`.cwd(dir) if (await published(name, version)) { console.log(`already published ${name}@${version}`) return } - if (process.platform !== "win32") await $`chmod -R 755 .`.cwd(dir) await $`bun pm pack`.cwd(dir) await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir) } From e543acf9230347f5642dbabffde35a493451748a Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Sun, 19 Apr 2026 12:35:02 +1000 Subject: [PATCH 207/335] chore: bump electron and fix taskbar icon (#23368) --- bun.lock | 4 ++-- package.json | 2 +- packages/desktop-electron/package.json | 2 +- packages/desktop-electron/src/main/index.ts | 4 +++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/bun.lock b/bun.lock index 947e355d7f..1cc7e4a4a6 100644 --- a/bun.lock +++ b/bun.lock @@ -248,7 +248,7 @@ "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "@valibot/to-json-schema": "1.6.0", - "electron": "40.4.1", + "electron": "41.2.1", "electron-builder": "^26", "electron-vite": "^5", "solid-js": "catalog:", @@ -3026,7 +3026,7 @@ "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], - "electron": ["electron@40.4.1", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-N1ZXybQZL8kYemO8vAeh9nrk4mSvqlAO8xs0QCHkXIvRnuB/7VGwEehjvQbsU5/f4bmTKpG+2GQERe/zmKpudQ=="], + "electron": ["electron@41.2.1", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-teeRThiYGTPKf/2yOW7zZA1bhb91KEQ4yLBPOg7GxpmnkLFLugKgQaAKOrCgdzwsXh/5mFIfmkm+4+wACJKwaA=="], "electron-builder": ["electron-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw=="], diff --git a/package.json b/package.json index ddd711adaf..063226ad0c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "packageManager": "bun@1.3.11", "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", - "dev:desktop": "bun --cwd packages/desktop tauri dev", + "dev:desktop": "bun --cwd packages/desktop-electron dev", "dev:web": "bun --cwd packages/app dev", "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev", "dev:storybook": "bun --cwd packages/storybook storybook", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index 8142b12ada..33366f3587 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -45,7 +45,7 @@ "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "@valibot/to-json-schema": "1.6.0", - "electron": "40.4.1", + "electron": "41.2.1", "electron-builder": "^26", "electron-vite": "^5", "solid-js": "catalog:", diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts index 946e01e325..8f21e5b933 100644 --- a/packages/desktop-electron/src/main/index.ts +++ b/packages/desktop-electron/src/main/index.ts @@ -28,8 +28,10 @@ const APP_IDS: Record = { beta: "ai.opencode.desktop.beta", prod: "ai.opencode.desktop", } +const appId = app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev" app.setName(app.isPackaged ? APP_NAMES[CHANNEL] : "OpenCode Dev") -app.setPath("userData", join(app.getPath("appData"), app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev")) +app.setAppUserModelId(appId) +app.setPath("userData", join(app.getPath("appData"), appId)) const { autoUpdater } = pkg import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types" From 40ba8f357024351c7437d8227e18906a2aadf824 Mon Sep 17 00:00:00 2001 From: opencode Date: Sun, 19 Apr 2026 03:02:14 +0000 Subject: [PATCH 208/335] sync release versions for v1.14.17 --- bun.lock | 32 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop-electron/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++----- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/shared/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 19 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 1cc7e4a4a6..b1765bf684 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.4.11", + "version": "1.14.17", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -83,7 +83,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.4.11", + "version": "1.14.17", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -117,7 +117,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.4.11", + "version": "1.14.17", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -144,7 +144,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.4.11", + "version": "1.14.17", "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/openai": "3.0.48", @@ -168,7 +168,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.4.11", + "version": "1.14.17", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -192,7 +192,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.4.11", + "version": "1.14.17", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -225,7 +225,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "1.4.11", + "version": "1.14.17", "dependencies": { "effect": "catalog:", "electron-context-menu": "4.1.2", @@ -268,7 +268,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.4.11", + "version": "1.14.17", "dependencies": { "@opencode-ai/shared": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -297,7 +297,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.4.11", + "version": "1.14.17", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -313,7 +313,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.4.11", + "version": "1.14.17", "bin": { "opencode": "./bin/opencode", }, @@ -458,7 +458,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.4.11", + "version": "1.14.17", "dependencies": { "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", @@ -493,7 +493,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.4.11", + "version": "1.14.17", "dependencies": { "cross-spawn": "catalog:", }, @@ -508,7 +508,7 @@ }, "packages/shared": { "name": "@opencode-ai/shared", - "version": "1.4.11", + "version": "1.14.17", "bin": { "opencode": "./bin/opencode", }, @@ -532,7 +532,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.4.11", + "version": "1.14.17", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -567,7 +567,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.4.11", + "version": "1.14.17", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -616,7 +616,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.4.11", + "version": "1.14.17", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 5a1a4504ea..7d21a9f95b 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.4.11", + "version": "1.14.17", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 200a5e30e3..17dad03069 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.4.11", + "version": "1.14.17", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index f233726e69..f9a7ed89c4 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.4.11", + "version": "1.14.17", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 1142230bb7..0217aba668 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.4.11", + "version": "1.14.17", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 860150aa28..3b7019246d 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.4.11", + "version": "1.14.17", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index 33366f3587..d8e20eb06c 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "1.4.11", + "version": "1.14.17", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index a23342bdec..006fcc5baa 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.4.11", + "version": "1.14.17", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index f565159628..7d2fc530cd 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.4.11", + "version": "1.14.17", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 32039c097a..4b1b1ca722 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.4.11" +version = "1.14.17" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.17/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.17/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.17/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.17/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.17/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 5d4229f64f..0d654f6041 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.4.11", + "version": "1.14.17", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 40f25f8f7b..dc20ecfc53 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.4.11", + "version": "1.14.17", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 15cd2db6e2..d62a41bf72 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.4.11", + "version": "1.14.17", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 91d6647449..02b168cf39 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.4.11", + "version": "1.14.17", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/shared/package.json b/packages/shared/package.json index a8cd62886b..d658aaa468 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.4.11", + "version": "1.14.17", "name": "@opencode-ai/shared", "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index 8ca990ba58..a5d99a0d18 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.4.11", + "version": "1.14.17", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 98cb928b7b..aedde0a4fa 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.4.11", + "version": "1.14.17", "type": "module", "license": "MIT", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index 194f44ec03..51c09f10be 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.4.11", + "version": "1.14.17", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index f52135c206..474c1a9141 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.4.11", + "version": "1.14.17", "publisher": "sst-dev", "repository": { "type": "git", From b34ca44abed7eb214cdaff26467ff786d17da523 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:06:07 +1000 Subject: [PATCH 209/335] fix incorrect config directory by lazily loading electron-store (#23373) --- packages/desktop-electron/src/main/migrate.ts | 8 ++++---- packages/desktop-electron/src/main/server.ts | 12 ++++++------ packages/desktop-electron/src/main/store.ts | 6 ++++-- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/desktop-electron/src/main/migrate.ts b/packages/desktop-electron/src/main/migrate.ts index bad1349eeb..70e3dc9c75 100644 --- a/packages/desktop-electron/src/main/migrate.ts +++ b/packages/desktop-electron/src/main/migrate.ts @@ -4,7 +4,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs" import { homedir } from "node:os" import { join } from "node:path" import { CHANNEL } from "./constants" -import { getStore, store } from "./store" +import { getStore } from "./store" const TAURI_MIGRATED_KEY = "tauriMigrated" @@ -67,7 +67,7 @@ function migrateFile(datPath: string, filename: string) { } export function migrate() { - if (store.get(TAURI_MIGRATED_KEY)) { + if (getStore().get(TAURI_MIGRATED_KEY)) { log.log("tauri migration: already done, skipping") return } @@ -77,7 +77,7 @@ export function migrate() { if (!existsSync(dir)) { log.log("tauri migration: no tauri data directory found, nothing to migrate") - store.set(TAURI_MIGRATED_KEY, true) + getStore().set(TAURI_MIGRATED_KEY, true) return } @@ -87,5 +87,5 @@ export function migrate() { } log.log("tauri migration: complete") - store.set(TAURI_MIGRATED_KEY, true) + getStore().set(TAURI_MIGRATED_KEY, true) } diff --git a/packages/desktop-electron/src/main/server.ts b/packages/desktop-electron/src/main/server.ts index 5a6050013a..55dfdf6e9b 100644 --- a/packages/desktop-electron/src/main/server.ts +++ b/packages/desktop-electron/src/main/server.ts @@ -1,33 +1,33 @@ import { app } from "electron" import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants" import { getUserShell, loadShellEnv } from "./shell-env" -import { store } from "./store" +import { getStore } from "./store" export type WslConfig = { enabled: boolean } export type HealthCheck = { wait: Promise } export function getDefaultServerUrl(): string | null { - const value = store.get(DEFAULT_SERVER_URL_KEY) + const value = getStore().get(DEFAULT_SERVER_URL_KEY) return typeof value === "string" ? value : null } export function setDefaultServerUrl(url: string | null) { if (url) { - store.set(DEFAULT_SERVER_URL_KEY, url) + getStore().set(DEFAULT_SERVER_URL_KEY, url) return } - store.delete(DEFAULT_SERVER_URL_KEY) + getStore().delete(DEFAULT_SERVER_URL_KEY) } export function getWslConfig(): WslConfig { - const value = store.get(WSL_ENABLED_KEY) + const value = getStore().get(WSL_ENABLED_KEY) return { enabled: typeof value === "boolean" ? value : false } } export function setWslConfig(config: WslConfig) { - store.set(WSL_ENABLED_KEY, config.enabled) + getStore().set(WSL_ENABLED_KEY, config.enabled) } export async function spawnLocalServer(hostname: string, port: number, password: string) { diff --git a/packages/desktop-electron/src/main/store.ts b/packages/desktop-electron/src/main/store.ts index 709e820e25..61f0c0a493 100644 --- a/packages/desktop-electron/src/main/store.ts +++ b/packages/desktop-electron/src/main/store.ts @@ -4,6 +4,10 @@ import { SETTINGS_STORE } from "./constants" const cache = new Map() +// We cannot instantiate the electron-store at module load time because +// module import hoisting causes this to run before app.setPath("userData", ...) +// in index.ts has executed, which would result in files being written to the default directory +// (e.g. bad: %APPDATA%\@opencode-ai\desktop-electron\opencode.settings vs good: %APPDATA%\ai.opencode.desktop.dev\opencode.settings). export function getStore(name = SETTINGS_STORE) { const cached = cache.get(name) if (cached) return cached @@ -11,5 +15,3 @@ export function getStore(name = SETTINGS_STORE) { cache.set(name, next) return next } - -export const store = getStore(SETTINGS_STORE) From 9ed93715efde1db921b952448a4bc64f7a48ee1e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 19 Apr 2026 03:40:53 +0000 Subject: [PATCH 210/335] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 34b9095562..154f89518c 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-Spc0qzg4he0d0GwHcqj7uBmvK4DIF1tEbsZ6M+pxpWc=", - "aarch64-linux": "sha256-gwz/PKBbT+72hr7vUG28cdx4Z7/Sf06PNMr9JBjAYg0=", - "aarch64-darwin": "sha256-Lj8p9P/QzLqxiM1OVSwcbtTsms8AcW3A6H0575ERufw=", - "x86_64-darwin": "sha256-y0e+TnXj6wKDqSC5hQAWjpKadaFvL6GJ6Mba5anBM+Y=" + "x86_64-linux": "sha256-7xsf5pzeU+OwgqjZsuc1xl/zHybH5cjaOBYlu66K8VY=", + "aarch64-linux": "sha256-9+z+y/iY5mOzQh7XL8bMx7PycodUxKlxd7p0ZfIJ9x4=", + "aarch64-darwin": "sha256-jkFrynHYCGryYc1vD/yc3f9E1QFuh1V6fotlY4uS+9I=", + "x86_64-darwin": "sha256-+qazYGUFiLcJ58p1a/16YIQGuOiCPFD/uRM3o81xd1I=" } } From f14ac472a3ba96c1cf400223c5957d86ed7a4a2d Mon Sep 17 00:00:00 2001 From: Ariane Emory <97994360+ariane-emory@users.noreply.github.com> Date: Sun, 19 Apr 2026 00:24:23 -0400 Subject: [PATCH 211/335] docs: document --dangerously-skip-permissions CLI flag (#23371) --- packages/web/src/content/docs/cli.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index 786b9d3d94..612826cabf 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -349,6 +349,7 @@ opencode run --attach http://localhost:4096 "Explain async/await in JavaScript" | `--title` | | Title for the session (uses truncated prompt if no value provided) | | `--attach` | | Attach to a running opencode server (e.g., http://localhost:4096) | | `--port` | | Port for the local server (defaults to random port) | +| `--dangerously-skip-permissions` | | Auto-approve permissions that are not explicitly denied (dangerous!) | --- From 75960e3bf3db67c4eb763fed2e282ed2ca0e62b1 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 19 Apr 2026 04:25:23 +0000 Subject: [PATCH 212/335] chore: generate --- packages/web/src/content/docs/cli.mdx | 30 +++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index 612826cabf..fb1130fe50 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -335,21 +335,21 @@ opencode run --attach http://localhost:4096 "Explain async/await in JavaScript" #### Flags -| Flag | Short | Description | -| ------------ | ----- | ----------------------------------------------------------------------- | -| `--command` | | The command to run, use message for args | -| `--continue` | `-c` | Continue the last session | -| `--session` | `-s` | Session ID to continue | -| `--fork` | | Fork the session when continuing (use with `--continue` or `--session`) | -| `--share` | | Share the session | -| `--model` | `-m` | Model to use in the form of provider/model | -| `--agent` | | Agent to use | -| `--file` | `-f` | File(s) to attach to message | -| `--format` | | Format: default (formatted) or json (raw JSON events) | -| `--title` | | Title for the session (uses truncated prompt if no value provided) | -| `--attach` | | Attach to a running opencode server (e.g., http://localhost:4096) | -| `--port` | | Port for the local server (defaults to random port) | -| `--dangerously-skip-permissions` | | Auto-approve permissions that are not explicitly denied (dangerous!) | +| Flag | Short | Description | +| -------------------------------- | ----- | ----------------------------------------------------------------------- | +| `--command` | | The command to run, use message for args | +| `--continue` | `-c` | Continue the last session | +| `--session` | `-s` | Session ID to continue | +| `--fork` | | Fork the session when continuing (use with `--continue` or `--session`) | +| `--share` | | Share the session | +| `--model` | `-m` | Model to use in the form of provider/model | +| `--agent` | | Agent to use | +| `--file` | `-f` | File(s) to attach to message | +| `--format` | | Format: default (formatted) or json (raw JSON events) | +| `--title` | | Title for the session (uses truncated prompt if no value provided) | +| `--attach` | | Attach to a running opencode server (e.g., http://localhost:4096) | +| `--port` | | Port for the local server (defaults to random port) | +| `--dangerously-skip-permissions` | | Auto-approve permissions that are not explicitly denied (dangerous!) | --- From fc0588954b01ab421bb4833173fa48c422cea4d5 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sun, 19 Apr 2026 00:45:44 -0500 Subject: [PATCH 213/335] fix (#23385) --- .../console/app/src/routes/workspace/[id]/billing/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx index 11185436be..e6c11c181b 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx @@ -22,10 +22,10 @@ export default function () { + - From 40834fdf2feda0ed3eded2a5e4076b7c77204989 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 19 Apr 2026 01:57:16 -0400 Subject: [PATCH 214/335] core: allow users with credits but no payment method to access zen mode --- packages/console/app/src/routes/zen/util/handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 2e576eaf68..9093739b15 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -762,7 +762,7 @@ export async function handler( const billing = authInfo.billing const billingUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/billing` const membersUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/members` - if (!billing.paymentMethodID) throw new CreditsError(t("zen.api.error.noPaymentMethod", { billingUrl })) + if (!billing.paymentMethodID && billing.balance <= 0) throw new CreditsError(t("zen.api.error.noPaymentMethod", { billingUrl })) if (billing.balance <= 0) throw new CreditsError(t("zen.api.error.insufficientBalance", { billingUrl })) const now = new Date() From f02504bb803dbd9de4103c4ea509836a19801e61 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 19 Apr 2026 05:58:31 +0000 Subject: [PATCH 215/335] chore: generate --- packages/console/app/src/routes/zen/util/handler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 9093739b15..81c512b99a 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -762,7 +762,8 @@ export async function handler( const billing = authInfo.billing const billingUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/billing` const membersUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/members` - if (!billing.paymentMethodID && billing.balance <= 0) throw new CreditsError(t("zen.api.error.noPaymentMethod", { billingUrl })) + if (!billing.paymentMethodID && billing.balance <= 0) + throw new CreditsError(t("zen.api.error.noPaymentMethod", { billingUrl })) if (billing.balance <= 0) throw new CreditsError(t("zen.api.error.insufficientBalance", { billingUrl })) const now = new Date() From 135c8f0e99814fb2184b9da657914126d4a81115 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 19 Apr 2026 05:59:31 +0000 Subject: [PATCH 216/335] chore: generate --- bun.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index b1765bf684..bd553f8412 100644 --- a/bun.lock +++ b/bun.lock @@ -3306,7 +3306,7 @@ "get-tsconfig": ["get-tsconfig@4.13.8", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-J87BxkLXykmisLQ+KA4x2+O6rVf+PJrtFUO8lGyiRg4lyxJLJ8/v0sRAKdVZQOy6tR6lMRAF1NqzCf9BQijm0w=="], - "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#4af877d", {}, "anomalyco-ghostty-web-4af877d", "sha512-fbEK8mtr7ar4ySsF+JUGjhaZrane7dKphanN+SxHt5XXI6yLMAh/Hpf6sNCOyyVa2UlGCd7YpXG/T2v2RUAX+A=="], + "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#20bd361", {}, "anomalyco-ghostty-web-20bd361", "sha512-dW0nwaiBBcun9y5WJSvm3HxDLe5o9V0xLCndQvWonRVubU8CS1PHxZpLffyPt1YujPWC13ez03aWxcuKBPYYGQ=="], "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], From e60a6e3a829f3598aba3707efc843a5364f0dc69 Mon Sep 17 00:00:00 2001 From: Dax Date: Sun, 19 Apr 2026 02:19:40 -0400 Subject: [PATCH 217/335] fix: change Free download button text to Download (#23388) --- packages/console/app/src/i18n/ar.ts | 2 +- packages/console/app/src/i18n/br.ts | 2 +- packages/console/app/src/i18n/da.ts | 2 +- packages/console/app/src/i18n/de.ts | 2 +- packages/console/app/src/i18n/en.ts | 2 +- packages/console/app/src/i18n/es.ts | 2 +- packages/console/app/src/i18n/fr.ts | 2 +- packages/console/app/src/i18n/it.ts | 2 +- packages/console/app/src/i18n/ja.ts | 2 +- packages/console/app/src/i18n/ko.ts | 2 +- packages/console/app/src/i18n/no.ts | 2 +- packages/console/app/src/i18n/pl.ts | 2 +- packages/console/app/src/i18n/ru.ts | 2 +- packages/console/app/src/i18n/th.ts | 2 +- packages/console/app/src/i18n/tr.ts | 2 +- packages/console/app/src/i18n/zh.ts | 2 +- packages/console/app/src/i18n/zht.ts | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index 713703b256..f0fdf21804 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "المؤسسات", "nav.zen": "Zen", "nav.login": "تسجيل الدخول", - "nav.free": "مجانا", + "nav.free": "تحميل", "nav.home": "الرئيسية", "nav.openMenu": "فتح القائمة", "nav.getStartedFree": "ابدأ مجانا", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index bace965696..fa479288b6 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "Enterprise", "nav.zen": "Zen", "nav.login": "Entrar", - "nav.free": "Grátis", + "nav.free": "Download", "nav.home": "Início", "nav.openMenu": "Abrir menu", "nav.getStartedFree": "Começar grátis", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index c2d3bf3ba0..9814ece9b5 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "Enterprise", "nav.zen": "Zen", "nav.login": "Log ind", - "nav.free": "Gratis", + "nav.free": "Download", "nav.home": "Hjem", "nav.openMenu": "Åbn menu", "nav.getStartedFree": "Kom i gang gratis", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index e44335bab9..aa73614932 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "Enterprise", "nav.zen": "Zen", "nav.login": "Anmelden", - "nav.free": "Kostenlos", + "nav.free": "Download", "nav.home": "Startseite", "nav.openMenu": "Menü öffnen", "nav.getStartedFree": "Kostenlos starten", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index a9c3ca3cc5..86119a560c 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -8,7 +8,7 @@ export const dict = { "nav.zen": "Zen", "nav.go": "Go", "nav.login": "Login", - "nav.free": "Free", + "nav.free": "Download", "nav.home": "Home", "nav.openMenu": "Open menu", "nav.getStartedFree": "Get started for free", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index e2a0c271a5..bde2bc988e 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "Enterprise", "nav.zen": "Zen", "nav.login": "Iniciar sesión", - "nav.free": "Gratis", + "nav.free": "Descargar", "nav.home": "Inicio", "nav.openMenu": "Abrir menú", "nav.getStartedFree": "Empezar gratis", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index 5a353e8735..867390027f 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -12,7 +12,7 @@ export const dict = { "nav.enterprise": "Entreprise", "nav.zen": "Zen", "nav.login": "Se connecter", - "nav.free": "Gratuit", + "nav.free": "Télécharger", "nav.home": "Accueil", "nav.openMenu": "Ouvrir le menu", "nav.getStartedFree": "Commencer gratuitement", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index b19bff9788..3ca1935dd5 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "Enterprise", "nav.zen": "Zen", "nav.login": "Accedi", - "nav.free": "Gratis", + "nav.free": "Scarica", "nav.home": "Home", "nav.openMenu": "Apri menu", "nav.getStartedFree": "Inizia gratis", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index 1571345e5d..7d13dda95b 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "エンタープライズ", "nav.zen": "Zen", "nav.login": "ログイン", - "nav.free": "無料", + "nav.free": "ダウンロード", "nav.home": "ホーム", "nav.openMenu": "メニューを開く", "nav.getStartedFree": "無料ではじめる", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index 9ec9310314..f9ac2e7f38 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "엔터프라이즈", "nav.zen": "Zen", "nav.login": "로그인", - "nav.free": "무료", + "nav.free": "다운로드", "nav.home": "홈", "nav.openMenu": "메뉴 열기", "nav.getStartedFree": "무료로 시작하기", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index 132b85a6e2..b08386f4fe 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "Enterprise", "nav.zen": "Zen", "nav.login": "Logg inn", - "nav.free": "Gratis", + "nav.free": "Last ned", "nav.home": "Hjem", "nav.openMenu": "Åpne meny", "nav.getStartedFree": "Kom i gang gratis", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index 441dfc7bea..27d6a9e068 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -10,7 +10,7 @@ export const dict = { "nav.enterprise": "Enterprise", "nav.zen": "Zen", "nav.login": "Zaloguj się", - "nav.free": "Darmowe", + "nav.free": "Pobierz", "nav.home": "Strona główna", "nav.openMenu": "Otwórz menu", "nav.getStartedFree": "Zacznij za darmo", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index ef7bcacd8b..b4070a9638 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "Enterprise", "nav.zen": "Zen", "nav.login": "Войти", - "nav.free": "Бесплатно", + "nav.free": "Скачать", "nav.home": "Главная", "nav.openMenu": "Открыть меню", "nav.getStartedFree": "Начать бесплатно", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index d7d862d948..9455c983f5 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "องค์กร", "nav.zen": "Zen", "nav.login": "เข้าสู่ระบบ", - "nav.free": "ฟรี", + "nav.free": "ดาวน์โหลด", "nav.home": "หน้าหลัก", "nav.openMenu": "เปิดเมนู", "nav.getStartedFree": "เริ่มต้นฟรี", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index 13a074642d..a6459b9508 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "Kurumsal", "nav.zen": "Zen", "nav.login": "Giriş", - "nav.free": "Ücretsiz", + "nav.free": "İndir", "nav.home": "Ana sayfa", "nav.openMenu": "Menüyü aç", "nav.getStartedFree": "Ücretsiz başla", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index c84ea5cc6b..5aa82e6fa3 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "企业版", "nav.zen": "Zen", "nav.login": "登录", - "nav.free": "免费", + "nav.free": "下载", "nav.home": "首页", "nav.openMenu": "打开菜单", "nav.getStartedFree": "免费开始", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index 6a70a81c71..aaaa31386c 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "企業", "nav.zen": "Zen", "nav.login": "登入", - "nav.free": "免費", + "nav.free": "下載", "nav.home": "首頁", "nav.openMenu": "開啟選單", "nav.getStartedFree": "免費開始使用", From 7f3b64c7c49147143eb7c544a019c103d70b890f Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 19 Apr 2026 06:38:10 +0000 Subject: [PATCH 218/335] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 154f89518c..e000d18508 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-7xsf5pzeU+OwgqjZsuc1xl/zHybH5cjaOBYlu66K8VY=", - "aarch64-linux": "sha256-9+z+y/iY5mOzQh7XL8bMx7PycodUxKlxd7p0ZfIJ9x4=", - "aarch64-darwin": "sha256-jkFrynHYCGryYc1vD/yc3f9E1QFuh1V6fotlY4uS+9I=", - "x86_64-darwin": "sha256-+qazYGUFiLcJ58p1a/16YIQGuOiCPFD/uRM3o81xd1I=" + "x86_64-linux": "sha256-30Aoaa20+0N085S/D4EdMDGianKN057zn9KC6bd+MzY=", + "aarch64-linux": "sha256-mxc0GAgoUWJmsyq0ZZVEnFQlecLfJksSSPTnv5tN6lE=", + "aarch64-darwin": "sha256-NdYTjyVegUfXkfxuurPeshHVRuWNbMja1L5HFVYzdMQ=", + "x86_64-darwin": "sha256-Xsn/rfvJYy1XMO50cruqOBavCVytWJ38J6OB+woznMo=" } } From 889087c9662a41438867f2b2b7a974f58dfda245 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Sun, 19 Apr 2026 12:28:15 +0530 Subject: [PATCH 219/335] fix(ripgrep): restore native rg backend (#22773) Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com> --- bun.lock | 3 - nix/opencode.nix | 35 +- packages/opencode/package.json | 1 - packages/opencode/script/build.ts | 10 +- packages/opencode/src/file/ripgrep.ts | 655 ++++++++----------- packages/opencode/src/file/ripgrep.worker.ts | 102 --- 6 files changed, 299 insertions(+), 507 deletions(-) delete mode 100644 packages/opencode/src/file/ripgrep.worker.ts diff --git a/bun.lock b/bun.lock index bd553f8412..a8de831782 100644 --- a/bun.lock +++ b/bun.lock @@ -404,7 +404,6 @@ "opentui-spinner": "0.0.6", "partial-json": "0.1.7", "remeda": "catalog:", - "ripgrep": "0.3.1", "semver": "^7.6.3", "solid-js": "catalog:", "strip-ansi": "7.1.2", @@ -4482,8 +4481,6 @@ "rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], - "ripgrep": ["ripgrep@0.3.1", "", { "bin": { "rg": "lib/rg.mjs", "ripgrep": "lib/rg.mjs" } }, "sha512-6bDtNIBh1qPviVIU685/4uv0Ap5t8eS4wiJhy/tR2LdIeIey9CVasENlGS+ul3HnTmGANIp7AjnfsztsRmALfQ=="], - "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], "rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="], diff --git a/nix/opencode.nix b/nix/opencode.nix index 4deac157e2..b629d0b554 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -7,6 +7,7 @@ sysctl, makeBinaryWrapper, models-dev, + ripgrep, installShellFiles, versionCheckHook, writableTmpDirAsHomeHook, @@ -51,25 +52,25 @@ stdenvNoCC.mkDerivation (finalAttrs: { runHook postBuild ''; - installPhase = - '' - runHook preInstall + installPhase = '' + runHook preInstall - install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode - install -Dm644 schema.json $out/share/opencode/schema.json - '' - # bun runs sysctl to detect if dunning on rosetta2 - + lib.optionalString stdenvNoCC.hostPlatform.isDarwin '' - wrapProgram $out/bin/opencode \ - --prefix PATH : ${ - lib.makeBinPath [ - sysctl + install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode + install -Dm644 schema.json $out/share/opencode/schema.json + + wrapProgram $out/bin/opencode \ + --prefix PATH : ${ + lib.makeBinPath ( + [ + ripgrep ] - } - '' - + '' - runHook postInstall - ''; + # bun runs sysctl to detect if dunning on rosetta2 + ++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl + ) + } + + runHook postInstall + ''; postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) '' # trick yargs into also generating zsh completions diff --git a/packages/opencode/package.json b/packages/opencode/package.json index dc20ecfc53..119e490add 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -161,7 +161,6 @@ "opentui-spinner": "0.0.6", "partial-json": "0.1.7", "remeda": "catalog:", - "ripgrep": "0.3.1", "semver": "^7.6.3", "solid-js": "catalog:", "strip-ansi": "7.1.2", diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 5aa14d52cd..85e1e105f1 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -187,7 +187,6 @@ for (const item of targets) { const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js") const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath) const workerPath = "./src/cli/cmd/tui/worker.ts" - const rgPath = "./src/file/ripgrep.worker.ts" // Use platform-specific bunfs root path based on target OS const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/" @@ -212,19 +211,12 @@ for (const item of targets) { windows: {}, }, files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}, - entrypoints: [ - "./src/index.ts", - parserWorker, - workerPath, - rgPath, - ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : []), - ], + entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])], define: { OPENCODE_VERSION: `'${Script.version}'`, OPENCODE_MIGRATIONS: JSON.stringify(migrations), OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath, OPENCODE_WORKER_PATH: workerPath, - OPENCODE_RIPGREP_WORKER_PATH: rgPath, OPENCODE_CHANNEL: `'${Script.channel}'`, OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "", }, diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index 3f16f6c501..c84d9b522a 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -1,15 +1,28 @@ -import fs from "fs/promises" import path from "path" -import { fileURLToPath } from "url" import z from "zod" -import { Cause, Context, Effect, Layer, Queue, Stream } from "effect" -import { ripgrep } from "ripgrep" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { Cause, Context, Effect, Fiber, Layer, Queue, Stream } from "effect" +import type { PlatformError } from "effect/PlatformError" +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" +import { ChildProcess } from "effect/unstable/process" +import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" -import { Filesystem } from "@/util" +import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { Global } from "@/global" import { Log } from "@/util" import { sanitizedProcessEnv } from "@/util/opencode-process" +import { which } from "@/util/which" const log = Log.create({ service: "ripgrep" }) +const VERSION = "14.1.1" +const PLATFORM = { + "arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" }, + "arm64-linux": { platform: "aarch64-unknown-linux-gnu", extension: "tar.gz" }, + "x64-darwin": { platform: "x86_64-apple-darwin", extension: "tar.gz" }, + "x64-linux": { platform: "x86_64-unknown-linux-musl", extension: "tar.gz" }, + "arm64-win32": { platform: "aarch64-pc-windows-msvc", extension: "zip" }, + "x64-win32": { platform: "x86_64-pc-windows-msvc", extension: "zip" }, +} as const const Stats = z.object({ elapsed: z.object({ @@ -121,62 +134,20 @@ export interface TreeInput { } export interface Interface { - readonly files: (input: FilesInput) => Stream.Stream - readonly tree: (input: TreeInput) => Effect.Effect - readonly search: (input: SearchInput) => Effect.Effect + readonly files: (input: FilesInput) => Stream.Stream + readonly tree: (input: TreeInput) => Effect.Effect + readonly search: (input: SearchInput) => Effect.Effect } export class Service extends Context.Service()("@opencode/Ripgrep") {} -type Run = { kind: "files" | "search"; cwd: string; args: string[] } - -type WorkerResult = { - type: "result" - code: number - stdout: string - stderr: string -} - -type WorkerLine = { - type: "line" - line: string -} - -type WorkerDone = { - type: "done" - code: number - stderr: string -} - -type WorkerError = { - type: "error" - error: { - message: string - name?: string - stack?: string - } -} - function env() { const env = sanitizedProcessEnv() delete env.RIPGREP_CONFIG_PATH return env } -function text(input: unknown) { - if (typeof input === "string") return input - if (input instanceof ArrayBuffer) return Buffer.from(input).toString() - if (ArrayBuffer.isView(input)) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString() - return String(input) -} - -function toError(input: unknown) { - if (input instanceof Error) return input - if (typeof input === "string") return new Error(input) - return new Error(String(input)) -} - -function abort(signal?: AbortSignal) { +function aborted(signal?: AbortSignal) { const err = signal?.reason if (err instanceof Error) return err const out = new Error("Aborted") @@ -184,6 +155,16 @@ function abort(signal?: AbortSignal) { return out } +function waitForAbort(signal?: AbortSignal) { + if (!signal) return Effect.never + if (signal.aborted) return Effect.fail(aborted(signal)) + return Effect.callback((resume) => { + const onabort = () => resume(Effect.fail(aborted(signal))) + signal.addEventListener("abort", onabort, { once: true }) + return Effect.sync(() => signal.removeEventListener("abort", onabort)) + }) +} + function error(stderr: string, code: number) { const err = new Error(stderr.trim() || `ripgrep failed with code ${code}`) err.name = "RipgrepError" @@ -204,371 +185,295 @@ function row(data: Row): Row { } } -function opts(cwd: string) { - return { - env: env(), - preopens: { ".": cwd }, - } +function parse(line: string) { + return Effect.try({ + try: () => Result.parse(JSON.parse(line)), + catch: (cause) => new Error("invalid ripgrep output", { cause }), + }) } -function check(cwd: string) { - return Effect.tryPromise({ - try: () => fs.stat(cwd).catch(() => undefined), - catch: toError, - }).pipe( - Effect.flatMap((stat) => - stat?.isDirectory() - ? Effect.void - : Effect.fail( - Object.assign(new Error(`No such file or directory: '${cwd}'`), { - code: "ENOENT", - errno: -2, - path: cwd, - }), - ), - ), - ) +function fail(queue: Queue.Queue, err: PlatformError | Error) { + Queue.failCauseUnsafe(queue, Cause.fail(err)) } function filesArgs(input: FilesInput) { - const args = ["--files", "--glob=!.git/*"] + const args = ["--no-config", "--files", "--glob=!.git/*"] if (input.follow) args.push("--follow") if (input.hidden !== false) args.push("--hidden") + if (input.hidden === false) args.push("--glob=!.*") if (input.maxDepth !== undefined) args.push(`--max-depth=${input.maxDepth}`) if (input.glob) { - for (const glob of input.glob) { - args.push(`--glob=${glob}`) - } + for (const glob of input.glob) args.push(`--glob=${glob}`) } args.push(".") return args } function searchArgs(input: SearchInput) { - const args = ["--json", "--hidden", "--glob=!.git/*", "--no-messages"] + const args = ["--no-config", "--json", "--hidden", "--glob=!.git/*", "--no-messages"] if (input.follow) args.push("--follow") if (input.glob) { - for (const glob of input.glob) { - args.push(`--glob=${glob}`) - } + for (const glob of input.glob) args.push(`--glob=${glob}`) } if (input.limit) args.push(`--max-count=${input.limit}`) args.push("--", input.pattern, ...(input.file ?? ["."])) return args } -function parse(stdout: string) { - return stdout - .trim() - .split(/\r?\n/) - .filter(Boolean) - .map((line) => Result.parse(JSON.parse(line))) - .flatMap((item) => (item.type === "match" ? [row(item.data)] : [])) +function raceAbort(effect: Effect.Effect, signal?: AbortSignal) { + return signal ? effect.pipe(Effect.raceFirst(waitForAbort(signal))) : effect } -declare const OPENCODE_RIPGREP_WORKER_PATH: string +export const layer: Layer.Layer = + Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const http = HttpClient.filterStatusOk(yield* HttpClient.HttpClient) + const spawner = yield* ChildProcessSpawner -function target(): Effect.Effect { - if (typeof OPENCODE_RIPGREP_WORKER_PATH !== "undefined") { - return Effect.succeed(OPENCODE_RIPGREP_WORKER_PATH) - } - const js = new URL("./ripgrep.worker.js", import.meta.url) - return Effect.tryPromise({ - try: () => Filesystem.exists(fileURLToPath(js)), - catch: toError, - }).pipe(Effect.map((exists) => (exists ? js : new URL("./ripgrep.worker.ts", import.meta.url)))) -} + const run = Effect.fnUntraced(function* (command: string, args: string[], opts?: { cwd?: string }) { + const handle = yield* spawner.spawn( + ChildProcess.make(command, args, { cwd: opts?.cwd, extendEnv: true, stdin: "ignore" }), + ) + const [stdout, stderr, code] = yield* Effect.all( + [ + Stream.mkString(Stream.decodeText(handle.stdout)), + Stream.mkString(Stream.decodeText(handle.stderr)), + handle.exitCode, + ], + { concurrency: "unbounded" }, + ) + return { stdout, stderr, code } + }, Effect.scoped) -function worker() { - return target().pipe(Effect.flatMap((file) => Effect.sync(() => new Worker(file, { env: env() })))) -} + const extract = Effect.fnUntraced(function* (archive: string, config: (typeof PLATFORM)[keyof typeof PLATFORM]) { + const dir = yield* fs.makeTempDirectoryScoped({ directory: Global.Path.bin, prefix: "ripgrep-" }) -function drain(buf: string, chunk: unknown, push: (line: string) => void) { - const lines = (buf + text(chunk)).split(/\r?\n/) - buf = lines.pop() || "" - for (const line of lines) { - if (line) push(line) - } - return buf -} - -function fail(queue: Queue.Queue, err: Error) { - Queue.failCauseUnsafe(queue, Cause.fail(err)) -} - -function searchDirect(input: SearchInput) { - return Effect.tryPromise({ - try: () => - ripgrep(searchArgs(input), { - buffer: true, - ...opts(input.cwd), - }), - catch: toError, - }).pipe( - Effect.flatMap((ret) => { - const out = ret.stdout ?? "" - if (ret.code !== 0 && ret.code !== 1 && ret.code !== 2) { - return Effect.fail(error(ret.stderr ?? "", ret.code ?? 1)) - } - return Effect.sync(() => ({ - items: ret.code === 1 ? [] : parse(out), - partial: ret.code === 2, - })) - }), - ) -} - -function searchWorker(input: SearchInput) { - if (input.signal?.aborted) return Effect.fail(abort(input.signal)) - - return Effect.acquireUseRelease( - worker(), - (w) => - Effect.callback((resume, signal) => { - let open = true - const done = (effect: Effect.Effect) => { - if (!open) return - open = false - resume(effect) + if (config.extension === "zip") { + const shell = (yield* Effect.sync(() => which("powershell.exe") ?? which("pwsh.exe"))) ?? "powershell.exe" + const result = yield* run(shell, [ + "-NoProfile", + "-Command", + "Expand-Archive -LiteralPath $args[0] -DestinationPath $args[1] -Force", + archive, + dir, + ]) + if (result.code !== 0) { + return yield* Effect.fail(error(result.stderr || result.stdout, result.code)) + } } - const onabort = () => done(Effect.fail(abort(input.signal))) - w.onerror = (evt) => { - done(Effect.fail(toError(evt.error ?? evt.message))) + if (config.extension === "tar.gz") { + const result = yield* run("tar", ["-xzf", archive, "-C", dir]) + if (result.code !== 0) { + return yield* Effect.fail(error(result.stderr || result.stdout, result.code)) + } } - w.onmessage = (evt: MessageEvent) => { - const msg = evt.data - if (msg.type === "error") { - done(Effect.fail(Object.assign(new Error(msg.error.message), msg.error))) - return + + return path.join(dir, `ripgrep-${VERSION}-${config.platform}`, process.platform === "win32" ? "rg.exe" : "rg") + }, Effect.scoped) + + const filepath = yield* Effect.cached( + Effect.gen(function* () { + const system = yield* Effect.sync(() => which("rg")) + if (system && (yield* fs.isFile(system).pipe(Effect.orDie))) return system + + const target = path.join(Global.Path.bin, `rg${process.platform === "win32" ? ".exe" : ""}`) + if (yield* fs.isFile(target).pipe(Effect.orDie)) return target + + const platformKey = `${process.arch}-${process.platform}` as keyof typeof PLATFORM + const config = PLATFORM[platformKey] + if (!config) { + return yield* Effect.fail(new Error(`unsupported platform for ripgrep: ${platformKey}`)) } - if (msg.code === 1) { - done(Effect.succeed({ items: [], partial: false })) - return + + const filename = `ripgrep-${VERSION}-${config.platform}.${config.extension}` + const url = `https://github.com/BurntSushi/ripgrep/releases/download/${VERSION}/${filename}` + const archive = path.join(Global.Path.bin, filename) + + log.info("downloading ripgrep", { url }) + yield* fs.ensureDir(Global.Path.bin).pipe(Effect.orDie) + + const bytes = yield* HttpClientRequest.get(url).pipe( + http.execute, + Effect.flatMap((response) => response.arrayBuffer), + Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))), + ) + if (bytes.byteLength === 0) { + return yield* Effect.fail(new Error(`failed to download ripgrep from ${url}`)) } - if (msg.code !== 0 && msg.code !== 1 && msg.code !== 2) { - done(Effect.fail(error(msg.stderr, msg.code))) - return + + yield* fs.writeWithDirs(archive, new Uint8Array(bytes)).pipe(Effect.orDie) + const extracted = yield* extract(archive, config) + const exists = yield* fs.exists(extracted).pipe(Effect.orDie) + if (!exists) { + return yield* Effect.fail(new Error(`ripgrep archive did not contain executable: ${extracted}`)) } - done( - Effect.sync(() => ({ - items: parse(msg.stdout), - partial: msg.code === 2, - })), + + yield* fs.copyFile(extracted, target).pipe(Effect.orDie) + if (process.platform !== "win32") { + yield* fs.chmod(target, 0o755).pipe(Effect.orDie) + } + yield* fs.remove(archive, { force: true }).pipe(Effect.ignore) + return target + }), + ) + + const check = Effect.fnUntraced(function* (cwd: string) { + if (yield* fs.isDir(cwd).pipe(Effect.orDie)) return + return yield* Effect.fail( + Object.assign(new Error(`No such file or directory: '${cwd}'`), { + code: "ENOENT", + errno: -2, + path: cwd, + }), + ) + }) + + const command = Effect.fnUntraced(function* (cwd: string, args: string[]) { + const binary = yield* filepath + return ChildProcess.make(binary, args, { + cwd, + env: env(), + extendEnv: true, + stdin: "ignore", + }) + }) + + const files: Interface["files"] = (input) => + Stream.callback((queue) => + Effect.gen(function* () { + yield* Effect.forkScoped( + Effect.gen(function* () { + yield* check(input.cwd) + const handle = yield* spawner.spawn(yield* command(input.cwd, filesArgs(input))) + const stderr = yield* Stream.mkString(Stream.decodeText(handle.stderr)).pipe(Effect.forkScoped) + const stdout = yield* Stream.decodeText(handle.stdout).pipe( + Stream.splitLines, + Stream.filter((line) => line.length > 0), + Stream.runForEach((line) => Effect.sync(() => Queue.offerUnsafe(queue, clean(line)))), + Effect.forkScoped, + ) + const code = yield* raceAbort(handle.exitCode, input.signal) + yield* Fiber.join(stdout) + if (code === 0 || code === 1) { + Queue.endUnsafe(queue) + return + } + fail(queue, error(yield* Fiber.join(stderr), code)) + }).pipe( + Effect.catch((err) => + Effect.sync(() => { + fail(queue, err) + }), + ), + ), + ) + }), + ) + + const search: Interface["search"] = Effect.fn("Ripgrep.search")(function* (input: SearchInput) { + yield* check(input.cwd) + + const program = Effect.scoped( + Effect.gen(function* () { + const handle = yield* spawner.spawn(yield* command(input.cwd, searchArgs(input))) + + const [items, stderr, code] = yield* Effect.all( + [ + Stream.decodeText(handle.stdout).pipe( + Stream.splitLines, + Stream.filter((line) => line.length > 0), + Stream.mapEffect(parse), + Stream.filter((item): item is Match => item.type === "match"), + Stream.map((item) => row(item.data)), + Stream.runCollect, + Effect.map((chunk) => [...chunk]), + ), + Stream.mkString(Stream.decodeText(handle.stderr)), + handle.exitCode, + ], + { concurrency: "unbounded" }, + ) + + if (code !== 0 && code !== 1 && code !== 2) { + return yield* Effect.fail(error(stderr, code)) + } + + return { + items: code === 1 ? [] : items, + partial: code === 2, + } + }), + ) + + return yield* raceAbort(program, input.signal) + }) + + const tree: Interface["tree"] = Effect.fn("Ripgrep.tree")(function* (input: TreeInput) { + log.info("tree", input) + const list = Array.from(yield* files({ cwd: input.cwd, signal: input.signal }).pipe(Stream.runCollect)) + + interface Node { + name: string + children: Map + } + + function child(node: Node, name: string) { + const item = node.children.get(name) + if (item) return item + const next = { name, children: new Map() } + node.children.set(name, next) + return next + } + + function count(node: Node): number { + return Array.from(node.children.values()).reduce((sum, child) => sum + 1 + count(child), 0) + } + + const root: Node = { name: "", children: new Map() } + for (const file of list) { + if (file.includes(".opencode")) continue + const parts = file.split(path.sep) + if (parts.length < 2) continue + let node = root + for (const part of parts.slice(0, -1)) { + node = child(node, part) + } + } + + const total = count(root) + const limit = input.limit ?? total + const lines: string[] = [] + const queue: Array<{ node: Node; path: string }> = Array.from(root.children.values()) + .sort((a, b) => a.name.localeCompare(b.name)) + .map((node) => ({ node, path: node.name })) + + let used = 0 + for (let i = 0; i < queue.length && used < limit; i++) { + const item = queue[i] + lines.push(item.path) + used++ + queue.push( + ...Array.from(item.node.children.values()) + .sort((a, b) => a.name.localeCompare(b.name)) + .map((node) => ({ node, path: `${item.path}/${node.name}` })), ) } - input.signal?.addEventListener("abort", onabort, { once: true }) - signal.addEventListener("abort", onabort, { once: true }) - w.postMessage({ - kind: "search", - cwd: input.cwd, - args: searchArgs(input), - } satisfies Run) + if (total > used) lines.push(`[${total - used} truncated]`) + return lines.join("\n") + }) - return Effect.sync(() => { - input.signal?.removeEventListener("abort", onabort) - signal.removeEventListener("abort", onabort) - w.onerror = null - w.onmessage = null - }) - }), - (w) => Effect.sync(() => w.terminate()), - ) -} - -function filesDirect(input: FilesInput) { - return Stream.callback( - Effect.fnUntraced(function* (queue: Queue.Queue) { - let buf = "" - let err = "" - - const out = { - write(chunk: unknown) { - buf = drain(buf, chunk, (line) => { - Queue.offerUnsafe(queue, clean(line)) - }) - }, - } - - const stderr = { - write(chunk: unknown) { - err += text(chunk) - }, - } - - yield* Effect.forkScoped( - Effect.gen(function* () { - yield* check(input.cwd) - const ret = yield* Effect.tryPromise({ - try: () => - ripgrep(filesArgs(input), { - stdout: out, - stderr, - ...opts(input.cwd), - }), - catch: toError, - }) - if (buf) Queue.offerUnsafe(queue, clean(buf)) - if (ret.code === 0 || ret.code === 1) { - Queue.endUnsafe(queue) - return - } - fail(queue, error(err, ret.code ?? 1)) - }).pipe( - Effect.catch((err) => - Effect.sync(() => { - fail(queue, err) - }), - ), - ), - ) + return Service.of({ files, tree, search }) }), ) -} -function filesWorker(input: FilesInput) { - return Stream.callback( - Effect.fnUntraced(function* (queue: Queue.Queue) { - if (input.signal?.aborted) { - fail(queue, abort(input.signal)) - return - } - - const w = yield* Effect.acquireRelease(worker(), (w) => Effect.sync(() => w.terminate())) - let open = true - const close = () => { - if (!open) return false - open = false - return true - } - const onabort = () => { - if (!close()) return - fail(queue, abort(input.signal)) - } - - w.onerror = (evt) => { - if (!close()) return - fail(queue, toError(evt.error ?? evt.message)) - } - w.onmessage = (evt: MessageEvent) => { - const msg = evt.data - if (msg.type === "line") { - if (open) Queue.offerUnsafe(queue, msg.line) - return - } - if (!close()) return - if (msg.type === "error") { - fail(queue, Object.assign(new Error(msg.error.message), msg.error)) - return - } - if (msg.code === 0 || msg.code === 1) { - Queue.endUnsafe(queue) - return - } - fail(queue, error(msg.stderr, msg.code)) - } - - yield* Effect.acquireRelease( - Effect.sync(() => { - input.signal?.addEventListener("abort", onabort, { once: true }) - w.postMessage({ - kind: "files", - cwd: input.cwd, - args: filesArgs(input), - } satisfies Run) - }), - () => - Effect.sync(() => { - input.signal?.removeEventListener("abort", onabort) - w.onerror = null - w.onmessage = null - }), - ) - }), - ) -} - -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const source = (input: FilesInput) => { - const useWorker = !!input.signal && typeof Worker !== "undefined" - if (!useWorker && input.signal) { - log.warn("worker unavailable, ripgrep abort disabled") - } - return useWorker ? filesWorker(input) : filesDirect(input) - } - - const files: Interface["files"] = (input) => source(input) - - const tree: Interface["tree"] = Effect.fn("Ripgrep.tree")(function* (input: TreeInput) { - log.info("tree", input) - const list = Array.from(yield* source({ cwd: input.cwd, signal: input.signal }).pipe(Stream.runCollect)) - - interface Node { - name: string - children: Map - } - - function child(node: Node, name: string) { - const item = node.children.get(name) - if (item) return item - const next = { name, children: new Map() } - node.children.set(name, next) - return next - } - - function count(node: Node): number { - return Array.from(node.children.values()).reduce((sum, child) => sum + 1 + count(child), 0) - } - - const root: Node = { name: "", children: new Map() } - for (const file of list) { - if (file.includes(".opencode")) continue - const parts = file.split(path.sep) - if (parts.length < 2) continue - let node = root - for (const part of parts.slice(0, -1)) { - node = child(node, part) - } - } - - const total = count(root) - const limit = input.limit ?? total - const lines: string[] = [] - const queue: Array<{ node: Node; path: string }> = Array.from(root.children.values()) - .sort((a, b) => a.name.localeCompare(b.name)) - .map((node) => ({ node, path: node.name })) - - let used = 0 - for (let i = 0; i < queue.length && used < limit; i++) { - const item = queue[i] - lines.push(item.path) - used++ - queue.push( - ...Array.from(item.node.children.values()) - .sort((a, b) => a.name.localeCompare(b.name)) - .map((node) => ({ node, path: `${item.path}/${node.name}` })), - ) - } - - if (total > used) lines.push(`[${total - used} truncated]`) - return lines.join("\n") - }) - - const search: Interface["search"] = Effect.fn("Ripgrep.search")(function* (input: SearchInput) { - const useWorker = !!input.signal && typeof Worker !== "undefined" - if (!useWorker && input.signal) { - log.warn("worker unavailable, ripgrep abort disabled") - } - return yield* useWorker ? searchWorker(input) : searchDirect(input) - }) - - return Service.of({ files, tree, search }) - }), +export const defaultLayer = layer.pipe( + Layer.provide(FetchHttpClient.layer), + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(CrossSpawnSpawner.defaultLayer), ) -export const defaultLayer = layer - export * as Ripgrep from "./ripgrep" diff --git a/packages/opencode/src/file/ripgrep.worker.ts b/packages/opencode/src/file/ripgrep.worker.ts deleted file mode 100644 index 21a3aef5cc..0000000000 --- a/packages/opencode/src/file/ripgrep.worker.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { ripgrep } from "ripgrep" -import { sanitizedProcessEnv } from "@/util/opencode-process" - -function env() { - const env = sanitizedProcessEnv() - delete env.RIPGREP_CONFIG_PATH - return env -} - -function opts(cwd: string) { - return { - env: env(), - preopens: { ".": cwd }, - } -} - -type Run = { - kind: "files" | "search" - cwd: string - args: string[] -} - -function text(input: unknown) { - if (typeof input === "string") return input - if (input instanceof ArrayBuffer) return Buffer.from(input).toString() - if (ArrayBuffer.isView(input)) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString() - return String(input) -} - -function error(input: unknown) { - if (input instanceof Error) { - return { - message: input.message, - name: input.name, - stack: input.stack, - } - } - - return { - message: String(input), - } -} - -function clean(file: string) { - return file.replace(/^\.[\\/]/, "") -} - -onmessage = async (evt: MessageEvent) => { - const msg = evt.data - - try { - if (msg.kind === "search") { - const ret = await ripgrep(msg.args, { - buffer: true, - ...opts(msg.cwd), - }) - postMessage({ - type: "result", - code: ret.code ?? 0, - stdout: ret.stdout ?? "", - stderr: ret.stderr ?? "", - }) - return - } - - let buf = "" - let err = "" - const out = { - write(chunk: unknown) { - buf += text(chunk) - const lines = buf.split(/\r?\n/) - buf = lines.pop() || "" - for (const line of lines) { - if (line) postMessage({ type: "line", line: clean(line) }) - } - }, - } - const stderr = { - write(chunk: unknown) { - err += text(chunk) - }, - } - - const ret = await ripgrep(msg.args, { - stdout: out, - stderr, - ...opts(msg.cwd), - }) - - if (buf) postMessage({ type: "line", line: clean(buf) }) - postMessage({ - type: "done", - code: ret.code ?? 0, - stderr: err, - }) - } catch (err) { - postMessage({ - type: "error", - error: error(err), - }) - } -} From e998c9e9cb9973919468ca49f4962d1b37d3bfc3 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 19 Apr 2026 07:35:27 +0000 Subject: [PATCH 220/335] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index e000d18508..508db12481 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-30Aoaa20+0N085S/D4EdMDGianKN057zn9KC6bd+MzY=", - "aarch64-linux": "sha256-mxc0GAgoUWJmsyq0ZZVEnFQlecLfJksSSPTnv5tN6lE=", - "aarch64-darwin": "sha256-NdYTjyVegUfXkfxuurPeshHVRuWNbMja1L5HFVYzdMQ=", - "x86_64-darwin": "sha256-Xsn/rfvJYy1XMO50cruqOBavCVytWJ38J6OB+woznMo=" + "x86_64-linux": "sha256-TVmmwqC0xX73fa6pIpRIfAqOLq2YOMJVQDRwNWHalWQ=", + "aarch64-linux": "sha256-hC1U0/22VeWpTiXO4cX6YyHLgYnZXbH+nWHUWKgEPvI=", + "aarch64-darwin": "sha256-YvD+5u0PWW1a6X9ukwd+80xmjHQbZN0K9JHX97bz9k8=", + "x86_64-darwin": "sha256-9D8QGsuQwtqjX7zPr6CfaM4/2Gc8YH6+kZ+9N+f1lno=" } } From a546e88f37d1816adadf1e833a5fb4f39b7d56df Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Sun, 19 Apr 2026 15:53:47 +0800 Subject: [PATCH 221/335] fix(desktop-electron): run JSON migration before spawning sidecar (#23396) --- bun.lock | 1 + packages/desktop-electron/package.json | 1 + packages/desktop-electron/src/main/index.ts | 32 +++++++++++++++------ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/bun.lock b/bun.lock index a8de831782..582eca489e 100644 --- a/bun.lock +++ b/bun.lock @@ -227,6 +227,7 @@ "name": "@opencode-ai/desktop-electron", "version": "1.14.17", "dependencies": { + "drizzle-orm": "catalog:", "effect": "catalog:", "electron-context-menu": "4.1.2", "electron-log": "^5", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index d8e20eb06c..236d175911 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -30,6 +30,7 @@ "electron-store": "^10", "electron-updater": "^6", "electron-window-state": "^5.0.3", + "drizzle-orm": "catalog:", "marked": "^15" }, "devDependencies": { diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts index 8f21e5b933..6c4e6d5ca1 100644 --- a/packages/desktop-electron/src/main/index.ts +++ b/packages/desktop-electron/src/main/index.ts @@ -43,6 +43,7 @@ import { parseMarkdown } from "./markdown" import { createMenu } from "./menu" import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server" import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows" +import { drizzle } from "drizzle-orm/node-sqlite/driver" import type { Server } from "virtual:opencode-server" const initEmitter = new EventEmitter() @@ -139,15 +140,6 @@ async function initialize() { const url = `http://${hostname}:${port}` const password = randomUUID() - logger.log("spawning sidecar", { url }) - const { listener, health } = await spawnLocalServer(hostname, port, password) - server = listener - serverReady.resolve({ - url, - username: "opencode", - password, - }) - const loadingTask = (async () => { logger.log("sidecar connection started", { url }) @@ -158,10 +150,32 @@ async function initialize() { if (progress.type === "Done") sqliteDone?.resolve() }) + if (needsMigration) { + const { Database, JsonMigration } = await import("virtual:opencode-server") + await JsonMigration.run(drizzle({ client: Database.Client().$client }), { + progress: (event: { current: number; total: number }) => { + const percent = Math.round(event.current / event.total) * 100 + initEmitter.emit("sqlite", { type: "InProgress", value: percent }) + }, + }) + initEmitter.emit("sqlite", { type: "Done" }) + + sqliteDone?.resolve() + } + if (needsMigration) { await sqliteDone?.promise } + logger.log("spawning sidecar", { url }) + const { listener, health } = await spawnLocalServer(hostname, port, password) + server = listener + serverReady.resolve({ + url, + username: "opencode", + password, + }) + await Promise.race([ health.wait, delay(30_000).then(() => { From 8ee47a0533e61dc49c896aa2ce295e2e4e949168 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 19 Apr 2026 08:29:51 +0000 Subject: [PATCH 222/335] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 508db12481..042d0bb2e9 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-TVmmwqC0xX73fa6pIpRIfAqOLq2YOMJVQDRwNWHalWQ=", - "aarch64-linux": "sha256-hC1U0/22VeWpTiXO4cX6YyHLgYnZXbH+nWHUWKgEPvI=", - "aarch64-darwin": "sha256-YvD+5u0PWW1a6X9ukwd+80xmjHQbZN0K9JHX97bz9k8=", - "x86_64-darwin": "sha256-9D8QGsuQwtqjX7zPr6CfaM4/2Gc8YH6+kZ+9N+f1lno=" + "x86_64-linux": "sha256-i9TxYwWkJAR+kW6pbvhgQbRW9UYPtdrPQAGic4zPoa4=", + "aarch64-linux": "sha256-RYc/OYlETXUwkWBRDas+/P4cBW6zde4FqxxnMARu5vs=", + "aarch64-darwin": "sha256-jIhUOIRIQEa2WT62TVIedmRIhl/edhK8sbiAFvU3yCM=", + "x86_64-darwin": "sha256-xLGzaX7OofFlZzVgpORJR5QXD2u+54hp+t3cCfUtO84=" } } From 83227be0ca3653063adbea694f640a7f2371fa6d Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Sun, 19 Apr 2026 17:05:03 +0800 Subject: [PATCH 223/335] fix(version): remove --target flag from beta release creation (#23403) --- script/version.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/version.ts b/script/version.ts index c1ad021b69..0582f49a3f 100755 --- a/script/version.ts +++ b/script/version.ts @@ -20,7 +20,7 @@ if (!Script.preview) { output.push(`release=${release.databaseId}`) output.push(`tag=${release.tagName}`) } else if (Script.channel === "beta") { - await $`gh release create v${Script.version} -d --target ${sha} --title "v${Script.version}" --repo ${process.env.GH_REPO}` + await $`gh release create v${Script.version} -d --title "v${Script.version}" --repo ${process.env.GH_REPO}` const release = await $`gh release view v${Script.version} --json tagName,databaseId --repo ${process.env.GH_REPO}`.json() output.push(`release=${release.databaseId}`) From c09bcfe5314ff4d78d169a9a373450d19df9b407 Mon Sep 17 00:00:00 2001 From: opencode Date: Sun, 19 Apr 2026 09:36:56 +0000 Subject: [PATCH 224/335] sync release versions for v1.14.18 --- bun.lock | 32 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop-electron/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++----- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/shared/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 19 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 582eca489e..fd4544f7eb 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -83,7 +83,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -117,7 +117,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -144,7 +144,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/openai": "3.0.48", @@ -168,7 +168,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -192,7 +192,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -225,7 +225,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -269,7 +269,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@opencode-ai/shared": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -298,7 +298,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -314,7 +314,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.14.17", + "version": "1.14.18", "bin": { "opencode": "./bin/opencode", }, @@ -458,7 +458,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", @@ -493,7 +493,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "cross-spawn": "catalog:", }, @@ -508,7 +508,7 @@ }, "packages/shared": { "name": "@opencode-ai/shared", - "version": "1.14.17", + "version": "1.14.18", "bin": { "opencode": "./bin/opencode", }, @@ -532,7 +532,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -567,7 +567,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -616,7 +616,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 7d21a9f95b..a3081798ac 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.14.17", + "version": "1.14.18", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 17dad03069..6a837c3731 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.14.17", + "version": "1.14.18", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index f9a7ed89c4..9b92cf0b2b 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.14.17", + "version": "1.14.18", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 0217aba668..6fde7612d4 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.14.17", + "version": "1.14.18", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 3b7019246d..d45a849368 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index 236d175911..01c6e84f33 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "1.14.17", + "version": "1.14.18", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 006fcc5baa..d3642523ad 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.14.17", + "version": "1.14.18", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 7d2fc530cd..885d52b9b1 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.14.17", + "version": "1.14.18", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 4b1b1ca722..7ae4694fb6 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.14.17" +version = "1.14.18" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.17/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.17/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.17/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.17/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.17/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 0d654f6041..a9a935639c 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.14.17", + "version": "1.14.18", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 119e490add..6d5abbbbdb 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.17", + "version": "1.14.18", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index d62a41bf72..3beea3620b 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.14.17", + "version": "1.14.18", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 02b168cf39..f39b575c82 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.14.17", + "version": "1.14.18", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/shared/package.json b/packages/shared/package.json index d658aaa468..b7fffcadb9 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.17", + "version": "1.14.18", "name": "@opencode-ai/shared", "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index a5d99a0d18..39dc9ab3c5 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.14.17", + "version": "1.14.18", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index aedde0a4fa..723cda40d8 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.14.17", + "version": "1.14.18", "type": "module", "license": "MIT", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index 51c09f10be..51be7fe4a6 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.14.17", + "version": "1.14.18", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 474c1a9141..dfddfa9d07 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.14.17", + "version": "1.14.18", "publisher": "sst-dev", "repository": { "type": "git", From 10bd044c55600408f2bca606bb6ce37c88b459f9 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:46:34 +1000 Subject: [PATCH 225/335] feat: add terminal font settings and built-in Nerd Font (#23391) --- .../JetBrainsMonoNerdFontMono-Regular.woff2 | Bin 0 -> 1060580 bytes .../app/src/components/settings-general.tsx | 27 ++++++++++++++++++ packages/app/src/components/terminal.tsx | 6 ++-- packages/app/src/context/settings.tsx | 17 +++++++++++ packages/app/src/i18n/ar.ts | 4 ++- packages/app/src/i18n/br.ts | 4 ++- packages/app/src/i18n/bs.ts | 4 ++- packages/app/src/i18n/da.ts | 4 ++- packages/app/src/i18n/de.ts | 4 ++- packages/app/src/i18n/en.ts | 4 ++- packages/app/src/i18n/es.ts | 4 ++- packages/app/src/i18n/fr.ts | 4 ++- packages/app/src/i18n/ja.ts | 4 ++- packages/app/src/i18n/ko.ts | 4 ++- packages/app/src/i18n/no.ts | 4 ++- packages/app/src/i18n/pl.ts | 4 ++- packages/app/src/i18n/ru.ts | 4 ++- packages/app/src/i18n/th.ts | 4 ++- packages/app/src/i18n/tr.ts | 4 ++- packages/app/src/i18n/zh.ts | 4 ++- packages/app/src/i18n/zht.ts | 4 ++- packages/app/src/index.css | 7 +++++ 22 files changed, 105 insertions(+), 20 deletions(-) create mode 100644 packages/app/public/assets/JetBrainsMonoNerdFontMono-Regular.woff2 diff --git a/packages/app/public/assets/JetBrainsMonoNerdFontMono-Regular.woff2 b/packages/app/public/assets/JetBrainsMonoNerdFontMono-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..02a57c6f500a61edf5cc451f23e76bd92a034f2a GIT binary patch literal 1060580 zcmZ^~bx>T*6E3{?;_kYz5FCQLySuwP!4upag1cLg;10pv-7Uf0CFtI~zwi6+)}1;v z^Gr{l>ZzXYInSA@@luvz13&=)01W1R0M@^jx-Rk`b^$;u&j0KEUxg@)j5sKbf+RSC z2Hr$i#Y)m5ln1b70Sux=%t65fA_WDOXzXutLvcZaVS>60kb_?t0k9NsDHI5C!}cga zTG>&kWT@hnVU6CT2`i#Ry|O6C*~(qE z6^Cg_{`d#v|6KxXUcPO6-Q~DJ_>Z>Vx5=MnfwD5UjWnU)STL-vC?<>+hKlH7wB8V( zXbB$4Z|I?At*YwgrIux<4abOP&oxAp*s$h$w5C=J%GDHpnHpO0q2P)#J@qZo($hA= zSW}xItt&n4=4z*i_-Rh}bob6Y_WUDHd=Ppfzfh8;{1$IqE*diJ<_!bQ@d)M6dB=WC5<0vND5&zbm=SU^<(3) z-foZZ<*pb@x=a?_>K?~*Nm)H2*PI?zYPi{H`$J!u4MeLQAE_LFr9k>qu;AYk$?g5YGa1!)} z<@wfzGBgL$hHX1)Ded%fy%#?T>?X%OTS2UBQ{iPIS;U%8!UGm+#(o3y!;TxXLFZ)d z)4UN5hh-JSL1W?yikhK0rcG%<=*aH&pJu-e>K#@H$&^zYOK|sj+J&D&x0d)U^h0xv zbF=%!oX(*(jxahUn-Agq?5&3SIpqK3VkZ~#!sl@rh6}FBMBMnMuNUyD5M-vsrOv(e zxsn?@B;PPFGU7osF7C6K4JxG$$C)TWJiC=~oJlcpInK&mss<}_R`#+J3gdL z#A*$ElucA9rWLdl>3e3IUcvF7y_H+}8*wzd)lc!$ogs`W+r?4!&5Vl)ctwV=Ol+Gg zyliupbKRK-Df|TFr-HIvEz4J8PXkBf7(9=^8QJ*9B0}|6(9qSrC6{t2zS9esAyi1~ zD_Kfhk-(9int}A4v17g�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

;T~PRvNwxSLla)p^Paf+OLIgkTB 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

EypM*5AT4FUI!g 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

QJc|;p1_3v{_gsPv1hR^@qwolO8fA0g;`O{cl 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 z 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�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%0A3Pd)$W{P({~3JwI^aCJaJ#zpAwynaxpWaJ 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%0DFo@#tbbrB(@8TCMa2XhUX<)(tH**Mj+ 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

vo{sTnd8bm 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­sa8;G&}NNZ3@R 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

    2Z2?JSpjtDLcrliMb)EO=JaZ_qoJkoR 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"Hp?KcNrxW{QCte#B( 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 J0$eTt005~&6t4gP literal 0 HcmV?d00001 diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index c380fb69b3..490bc2e484 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -19,6 +19,9 @@ import { sansDefault, sansFontFamily, sansInput, + terminalDefault, + terminalFontFamily, + terminalInput, useSettings, } from "@/context/settings" import { decode64 } from "@/utils/base64" @@ -181,6 +184,7 @@ export const SettingsGeneral: Component = () => { const soundOptions = [noneSound, ...SOUND_OPTIONS] const mono = () => monoInput(settings.appearance.font()) const sans = () => sansInput(settings.appearance.uiFont()) + const terminal = () => terminalInput(settings.appearance.terminalFont()) const soundSelectProps = ( enabled: () => boolean, @@ -451,6 +455,29 @@ export const SettingsGeneral: Component = () => { />

+ + +
+ settings.appearance.setTerminalFont(value)} + placeholder={terminalDefault} + spellcheck={false} + autocorrect="off" + autocomplete="off" + autocapitalize="off" + class="text-12-regular" + style={{ "font-family": terminalFontFamily(settings.appearance.terminalFont()) }} + /> +
+
) diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 57e91d6d33..ff5ff9dada 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -11,7 +11,7 @@ import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { useSDK } from "@/context/sdk" import { useServer } from "@/context/server" -import { monoFontFamily, useSettings } from "@/context/settings" +import { terminalFontFamily, useSettings } from "@/context/settings" import type { LocalPTY } from "@/context/terminal" import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters" import { terminalWriter } from "@/utils/terminal-writer" @@ -300,7 +300,7 @@ export const Terminal = (props: TerminalProps) => { }) createEffect(() => { - const font = monoFontFamily(settings.appearance.font()) + const font = terminalFontFamily(settings.appearance.terminalFont()) if (!term) return setOptionIfSupported(term, "fontFamily", font) scheduleFit() @@ -360,7 +360,7 @@ export const Terminal = (props: TerminalProps) => { cols: restoreSize?.cols, rows: restoreSize?.rows, fontSize: 14, - fontFamily: monoFontFamily(settings.appearance.font()), + fontFamily: terminalFontFamily(settings.appearance.terminalFont()), allowTransparency: false, convertEol: false, theme: terminalColors(), diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index a585789ce4..3012f099e5 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -39,6 +39,7 @@ export interface Settings { fontSize: number mono: string sans: string + terminal: string } keybinds: Record permissions: { @@ -50,13 +51,16 @@ export interface Settings { export const monoDefault = "System Mono" export const sansDefault = "System Sans" +export const terminalDefault = "JetBrainsMono Nerd Font Mono" const monoFallback = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' +const terminalFallback = '"JetBrainsMono Nerd Font Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' const monoBase = monoFallback const sansBase = sansFallback +const terminalBase = terminalFallback function input(font: string | undefined) { return font ?? "" @@ -89,6 +93,14 @@ export function sansFontFamily(font: string | undefined) { return stack(font, sansBase) } +export function terminalInput(font: string | undefined) { + return input(font) +} + +export function terminalFontFamily(font: string | undefined) { + return stack(font, terminalBase) +} + const defaultSettings: Settings = { general: { autoSave: true, @@ -110,6 +122,7 @@ const defaultSettings: Settings = { fontSize: 14, mono: "", sans: "", + terminal: "", }, keybinds: {}, permissions: { @@ -233,6 +246,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont setUIFont(value: string) { setStore("appearance", "sans", value.trim() ? value : "") }, + terminalFont: withFallback(() => store.appearance?.terminal, defaultSettings.appearance.terminal), + setTerminalFont(value: string) { + setStore("appearance", "terminal", value.trim() ? value : "") + }, }, keybinds: { get: (action: string) => store.keybinds?.[action], diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 6c3f3bb55e..9e9a88c2d0 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -565,7 +565,9 @@ export const dict = { "settings.general.row.theme.title": "السمة", "settings.general.row.theme.description": "تخصيص سمة OpenCode.", "settings.general.row.font.title": "خط الكود", - "settings.general.row.font.description": "خصّص الخط المستخدم في كتل التعليمات البرمجية والطرفيات", + "settings.general.row.font.description": "خصّص الخط المستخدم في كتل التعليمات البرمجية", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", "settings.general.row.uiFont.title": "خط الواجهة", "settings.general.row.uiFont.description": "خصّص الخط المستخدم في الواجهة بأكملها", "settings.general.row.followup.title": "سلوك المتابعة", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 63880462a4..5fd1aee763 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -572,7 +572,9 @@ export const dict = { "settings.general.row.theme.title": "Tema", "settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.", "settings.general.row.font.title": "Fonte de código", - "settings.general.row.font.description": "Personalize a fonte usada em blocos de código e terminais", + "settings.general.row.font.description": "Personalize a fonte usada em blocos de código", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", "settings.general.row.uiFont.title": "Fonte da interface", "settings.general.row.uiFont.description": "Personalize a fonte usada em toda a interface", "settings.general.row.followup.title": "Comportamento de acompanhamento", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 2b589eb35f..f872db1f00 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -637,7 +637,9 @@ export const dict = { "settings.general.row.theme.title": "Tema", "settings.general.row.theme.description": "Prilagodi temu OpenCode-a.", "settings.general.row.font.title": "Font za kod", - "settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda i terminalima", + "settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", "settings.general.row.uiFont.title": "UI font", "settings.general.row.uiFont.description": "Prilagodi font koji se koristi u cijelom interfejsu", "settings.general.row.followup.title": "Ponašanje nadovezivanja", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index b096d87b4b..82f4fe3f63 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -632,7 +632,9 @@ export const dict = { "settings.general.row.theme.title": "Tema", "settings.general.row.theme.description": "Tilpas hvordan OpenCode er temabestemt.", "settings.general.row.font.title": "Kode-skrifttype", - "settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke og terminaler", + "settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", "settings.general.row.uiFont.title": "UI-skrifttype", "settings.general.row.uiFont.description": "Tilpas skrifttypen, der bruges i hele brugerfladen", "settings.general.row.followup.title": "Opfølgningsadfærd", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 6dc0b04972..d5b95459ac 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -582,7 +582,9 @@ export const dict = { "settings.general.row.theme.title": "Thema", "settings.general.row.theme.description": "Das Thema von OpenCode anpassen.", "settings.general.row.font.title": "Code-Schriftart", - "settings.general.row.font.description": "Die in Codeblöcken und Terminals verwendete Schriftart anpassen", + "settings.general.row.font.description": "Die in Codeblöcken verwendete Schriftart anpassen", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", "settings.general.row.uiFont.title": "UI-Schriftart", "settings.general.row.uiFont.description": "Die im gesamten Interface verwendete Schriftart anpassen", "settings.general.row.followup.title": "Verhalten bei Folgefragen", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 2cfb797148..8a2fbf87f0 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -735,7 +735,9 @@ export const dict = { "settings.general.row.theme.title": "Theme", "settings.general.row.theme.description": "Customise how OpenCode is themed.", "settings.general.row.font.title": "Code Font", - "settings.general.row.font.description": "Customise the font used in code blocks and terminals", + "settings.general.row.font.description": "Customise the font used in code blocks", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", "settings.general.row.uiFont.title": "UI Font", "settings.general.row.uiFont.description": "Customise the font used throughout the interface", "settings.general.row.followup.title": "Follow-up behavior", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index c600232ef6..12bc45cf38 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -640,7 +640,9 @@ export const dict = { "settings.general.row.theme.title": "Tema", "settings.general.row.theme.description": "Personaliza el tema de OpenCode.", "settings.general.row.font.title": "Fuente de código", - "settings.general.row.font.description": "Personaliza la fuente usada en bloques de código y terminales", + "settings.general.row.font.description": "Personaliza la fuente usada en bloques de código", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", "settings.general.row.uiFont.title": "Fuente de la interfaz", "settings.general.row.uiFont.description": "Personaliza la fuente usada en toda la interfaz", "settings.general.row.followup.title": "Comportamiento de seguimiento", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index a140c1e3a1..6c98b9ca1e 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -579,7 +579,9 @@ export const dict = { "settings.general.row.theme.title": "Thème", "settings.general.row.theme.description": "Personnaliser le thème d'OpenCode.", "settings.general.row.font.title": "Police de code", - "settings.general.row.font.description": "Personnaliser la police utilisée dans les blocs de code et les terminaux", + "settings.general.row.font.description": "Personnaliser la police utilisée dans les blocs de code", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", "settings.general.row.uiFont.title": "Police de l'interface", "settings.general.row.uiFont.description": "Personnaliser la police utilisée dans toute l'interface", "settings.general.row.followup.title": "Comportement de suivi", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 3da1c4b43b..7678334127 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -569,7 +569,9 @@ export const dict = { "settings.general.row.theme.title": "テーマ", "settings.general.row.theme.description": "OpenCodeのテーマをカスタマイズします。", "settings.general.row.font.title": "コードフォント", - "settings.general.row.font.description": "コードブロックとターミナルで使用するフォントをカスタマイズします", + "settings.general.row.font.description": "コードブロックで使用するフォントをカスタマイズします", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", "settings.general.row.uiFont.title": "UIフォント", "settings.general.row.uiFont.description": "インターフェース全体で使用するフォントをカスタマイズします", "settings.general.row.followup.title": "フォローアップの動作", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 2b5ccd43d9..76bf33df6f 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -566,7 +566,9 @@ export const dict = { "settings.general.row.theme.title": "테마", "settings.general.row.theme.description": "OpenCode 테마 사용자 지정", "settings.general.row.font.title": "코드 글꼴", - "settings.general.row.font.description": "코드 블록과 터미널에 사용되는 글꼴을 사용자 지정", + "settings.general.row.font.description": "코드 블록에 사용되는 글꼴을 사용자 지정", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", "settings.general.row.uiFont.title": "UI 글꼴", "settings.general.row.uiFont.description": "인터페이스 전반에 사용되는 글꼴을 사용자 지정", "settings.general.row.followup.title": "후속 조치 동작", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index a0a968179c..75e557b16b 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -640,7 +640,9 @@ export const dict = { "settings.general.row.theme.title": "Tema", "settings.general.row.theme.description": "Tilpass hvordan OpenCode er tematisert.", "settings.general.row.font.title": "Kodefont", - "settings.general.row.font.description": "Tilpass skrifttypen som brukes i kodeblokker og terminaler", + "settings.general.row.font.description": "Tilpass skrifttypen som brukes i kodeblokker", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", "settings.general.row.uiFont.title": "UI-skrift", "settings.general.row.uiFont.description": "Tilpass skrifttypen som brukes i hele grensesnittet", "settings.general.row.followup.title": "Oppfølgingsadferd", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 88d209f11f..0ab4a6906c 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -571,7 +571,9 @@ export const dict = { "settings.general.row.theme.title": "Motyw", "settings.general.row.theme.description": "Dostosuj motyw OpenCode.", "settings.general.row.font.title": "Czcionka kodu", - "settings.general.row.font.description": "Dostosuj czcionkę używaną w blokach kodu i terminalach", + "settings.general.row.font.description": "Dostosuj czcionkę używaną w blokach kodu", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", "settings.general.row.uiFont.title": "Czcionka interfejsu", "settings.general.row.uiFont.description": "Dostosuj czcionkę używaną w całym interfejsie", "settings.general.row.followup.title": "Zachowanie kontynuacji", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 688289b7e8..135c8e66c4 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -637,7 +637,9 @@ export const dict = { "settings.general.row.theme.title": "Тема", "settings.general.row.theme.description": "Настройте оформление OpenCode.", "settings.general.row.font.title": "Шрифт кода", - "settings.general.row.font.description": "Настройте шрифт, используемый в блоках кода и терминалах", + "settings.general.row.font.description": "Настройте шрифт, используемый в блоках кода", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", "settings.general.row.uiFont.title": "Шрифт интерфейса", "settings.general.row.uiFont.description": "Настройте шрифт, используемый во всем интерфейсе", "settings.general.row.followup.title": "Поведение уточняющих вопросов", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 5decf3adb5..81674df32d 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -631,7 +631,9 @@ export const dict = { "settings.general.row.theme.title": "ธีม", "settings.general.row.theme.description": "ปรับแต่งวิธีการที่ OpenCode มีธีม", "settings.general.row.font.title": "ฟอนต์โค้ด", - "settings.general.row.font.description": "ปรับแต่งฟอนต์ที่ใช้ในบล็อกโค้ดและเทอร์มินัล", + "settings.general.row.font.description": "ปรับแต่งฟอนต์ที่ใช้ในบล็อกโค้ด", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", "settings.general.row.uiFont.title": "ฟอนต์ UI", "settings.general.row.uiFont.description": "ปรับแต่งฟอนต์ที่ใช้ทั่วทั้งอินเทอร์เฟซ", "settings.general.row.followup.title": "พฤติกรรมการติดตามผล", diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts index 6a3ade0d0b..f3cb3ab464 100644 --- a/packages/app/src/i18n/tr.ts +++ b/packages/app/src/i18n/tr.ts @@ -644,7 +644,9 @@ export const dict = { "settings.general.row.theme.title": "Tema", "settings.general.row.theme.description": "OpenCode'un temasını özelleştirin.", "settings.general.row.font.title": "Kod Yazı Tipi", - "settings.general.row.font.description": "Kod bloklarında ve terminallerde kullanılan yazı tipini özelleştirin", + "settings.general.row.font.description": "Kod bloklarında kullanılan yazı tipini özelleştirin", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", "settings.general.row.uiFont.title": "Arayüz Yazı Tipi", "settings.general.row.uiFont.description": "Arayüz genelinde kullanılan yazı tipini özelleştirin", "settings.general.row.followup.title": "Takip davranışı", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 28231733ea..d95bfd19ba 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -631,7 +631,9 @@ export const dict = { "settings.general.row.theme.title": "主题", "settings.general.row.theme.description": "自定义 OpenCode 的主题。", "settings.general.row.font.title": "代码字体", - "settings.general.row.font.description": "自定义代码块和终端使用的字体", + "settings.general.row.font.description": "自定义代码块使用的字体", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", "settings.general.row.uiFont.title": "界面字体", "settings.general.row.uiFont.description": "自定义整个界面使用的字体", "settings.general.row.followup.title": "跟进消息行为", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 4abdf5db57..4a88ca4fc8 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -626,7 +626,9 @@ export const dict = { "settings.general.row.theme.title": "主題", "settings.general.row.theme.description": "自訂 OpenCode 的主題。", "settings.general.row.font.title": "程式碼字型", - "settings.general.row.font.description": "自訂程式碼區塊和終端機使用的字型", + "settings.general.row.font.description": "自訂程式碼區塊使用的字型", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", "settings.general.row.uiFont.title": "介面字型", "settings.general.row.uiFont.description": "自訂整個介面使用的字型", "settings.general.row.followup.title": "後續追問行為", diff --git a/packages/app/src/index.css b/packages/app/src/index.css index 629ac80a86..f247b5e017 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -1,5 +1,12 @@ @import "@opencode-ai/ui/styles/tailwind"; +@font-face { + font-family: 'JetBrainsMono Nerd Font Mono'; + src: url('/assets/JetBrainsMonoNerdFontMono-Regular.woff2') format('woff2'); + font-weight: normal; + font-style: normal; +} + @layer components { @keyframes session-progress-whip { 0% { From 33b2795cc84c79e91e15549609713567eb08348a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 19 Apr 2026 09:47:36 +0000 Subject: [PATCH 226/335] chore: generate --- packages/app/src/context/settings.tsx | 3 ++- packages/app/src/index.css | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index 3012f099e5..6d4f3d2cda 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -56,7 +56,8 @@ export const terminalDefault = "JetBrainsMono Nerd Font Mono" const monoFallback = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' -const terminalFallback = '"JetBrainsMono Nerd Font Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' +const terminalFallback = + '"JetBrainsMono Nerd Font Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' const monoBase = monoFallback const sansBase = sansFallback diff --git a/packages/app/src/index.css b/packages/app/src/index.css index f247b5e017..9fbc5be706 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -1,8 +1,8 @@ @import "@opencode-ai/ui/styles/tailwind"; @font-face { - font-family: 'JetBrainsMono Nerd Font Mono'; - src: url('/assets/JetBrainsMonoNerdFontMono-Regular.woff2') format('woff2'); + font-family: "JetBrainsMono Nerd Font Mono"; + src: url("/assets/JetBrainsMonoNerdFontMono-Regular.woff2") format("woff2"); font-weight: normal; font-style: normal; } From a824064c4c7c2b43e4b59da0f578932faca7b26a Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 20 Apr 2026 00:10:31 +0200 Subject: [PATCH 227/335] stabilize TUI theme persistence and KV writes (#23188) --- bun.lock | 28 ++++++------- packages/opencode/package.json | 4 +- packages/opencode/src/cli/cmd/tui/app.tsx | 8 +--- .../opencode/src/cli/cmd/tui/context/kv.tsx | 32 +++++++++++++-- .../src/cli/cmd/tui/context/theme.tsx | 12 ++++-- .../opencode/src/cli/cmd/tui/util/terminal.ts | 39 ------------------- packages/plugin/package.json | 8 ++-- 7 files changed, 57 insertions(+), 74 deletions(-) diff --git a/bun.lock b/bun.lock index fd4544f7eb..fc96627961 100644 --- a/bun.lock +++ b/bun.lock @@ -366,8 +366,8 @@ "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", - "@opentui/core": "catalog:", - "@opentui/solid": "catalog:", + "@opentui/core": "0.1.101", + "@opentui/solid": "0.1.101", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -465,16 +465,16 @@ "zod": "catalog:", }, "devDependencies": { - "@opentui/core": "catalog:", - "@opentui/solid": "catalog:", + "@opentui/core": "0.1.101", + "@opentui/solid": "0.1.101", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "typescript": "catalog:", }, "peerDependencies": { - "@opentui/core": ">=0.1.100", - "@opentui/solid": ">=0.1.100", + "@opentui/core": ">=0.1.101", + "@opentui/solid": ">=0.1.101", }, "optionalPeers": [ "@opentui/core", @@ -1600,21 +1600,21 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], - "@opentui/core": ["@opentui/core@0.1.99", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.99", "@opentui/core-darwin-x64": "0.1.99", "@opentui/core-linux-arm64": "0.1.99", "@opentui/core-linux-x64": "0.1.99", "@opentui/core-win32-arm64": "0.1.99", "@opentui/core-win32-x64": "0.1.99", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-I3+AEgGzqNWIpWX9g2WOscSPwtQDNOm4KlBjxBWCZjLxkF07u77heWXF7OiAdhKLtNUW6TFiyt6yznqAZPdG3A=="], + "@opentui/core": ["@opentui/core@0.1.101", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.101", "@opentui/core-darwin-x64": "0.1.101", "@opentui/core-linux-arm64": "0.1.101", "@opentui/core-linux-x64": "0.1.101", "@opentui/core-win32-arm64": "0.1.101", "@opentui/core-win32-x64": "0.1.101", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-8jUhNKnwCDO3Y2iiEmagoQLjgX5l1WbddQiwky8B5JU4FW0/WRHairBmU1kRAQBmhdeg57dVinSG4iu2PAtKEA=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.99", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bzVrqeX2vb5iWrc/ftOUOqeUY8XO+qSgoTwj5TXHuwagavgwD3Hpeyjx8+icnTTeM4pao0som1WR9xfye6/X5Q=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.101", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HtqZh8TIKCH1Nge5J0etBCpzYfPY4fVcq110uJm2As6D/dTTPv8r4J+KkrqoSphkpj/Y2b4t7KpqNHthXA0EVw=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.99", "", { "os": "darwin", "cpu": "x64" }, "sha512-VE4FrXBYpkxnvkqcCV1a8aN9jyyMJMihVW+V2NLCtp+4yQsj0AapG5TiUSN76XnmSZRptxDy5rBmEempeoIZbg=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.101", "", { "os": "darwin", "cpu": "x64" }, "sha512-o5ClQWnGG1inRE2YZAatPw1jPEAJni00amcoIfKBj8e1WS+fQA+iQTq1xFunNcyNPObLDCVuW1X+NrbK9xmPvQ=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.99", "", { "os": "linux", "cpu": "arm64" }, "sha512-viXQsbpS7yHjYkl7+am32JdvG96QU9lvHh1UiZtpOxcNUUqiYmA2ZwZFPD2Bi54jNyj5l2hjH6YkD3DzE2FEWA=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.101", "", { "os": "linux", "cpu": "arm64" }, "sha512-E/weY7DQpaPWGYDPD0CROHowUotqnVlk7Kb6l9+iZCrxm9s7HPRHkcMDVmcWDqHEqa/J879EJcqaUDzDArqC+w=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.99", "", { "os": "linux", "cpu": "x64" }, "sha512-WLoEFINOSp0tZSR9y4LUuGc7n4Y7H1wcpjUPzQ9vChkYDXrfZltEanzoDWbDcQ4kZQW5tHVC7LrZHpAsRLwFZg=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.101", "", { "os": "linux", "cpu": "x64" }, "sha512-+Bfr8jLbbR1WREUMCCvSZ44G1+WU2lPqJx7x1StTa9iFNEdicxCdd0QQsO6cnKn5yW+2Pr/FdrqHbxSQw3ejbA=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.99", "", { "os": "win32", "cpu": "arm64" }, "sha512-yWMOLWCEO8HdrctU1dMkgZC8qGkiO4Dwr4/e11tTvVpRmYhDsP/IR89ZjEEtOwnKwFOFuB/MxvflqaEWVQ2g5Q=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.101", "", { "os": "win32", "cpu": "arm64" }, "sha512-LTMIHJzJrVqS8mgpp+tuyVHuqYlicQTvFi/sTsJ6Xswf1asatsvZYsbQByhBLpFT80j10G7uvDa361S5gjCUDA=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.99", "", { "os": "win32", "cpu": "x64" }, "sha512-aYRlsL2w8YRL6vPd7/hrqlNVkXU3QowWb01TOvAcHS8UAsXaGFUr47kSDyjxDi1wg1MzmVduCfsC7T3NoThV1w=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.101", "", { "os": "win32", "cpu": "x64" }, "sha512-VaMs5bg6y0tYKptaEK8Hy5wTp4m//wJRKUdW8uvrS9cFgxyovZGuw0+TfK3NgbdeX+8jWm8LEAiak4jle5BABg=="], - "@opentui/solid": ["@opentui/solid@0.1.99", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.99", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-DrqqO4h2V88FmeIP2cErYkMU0ZK5MrUsZw3w6IzZpoXyyiL4/9qpWzUq+CXx+r16VP2iGxDJwGKUmtFAzUch2Q=="], + "@opentui/solid": ["@opentui/solid@0.1.101", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.101", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-STY2FQYtVS2rhUgpslG6mM0EAkgobBDF91+B+SNmvXIkJwP+ydP6UVgcuIo5McIbb9GIbAODx5X2Q48PSR7hgw=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 6d5abbbbdb..42f30b45ef 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -122,8 +122,8 @@ "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", - "@opentui/core": "catalog:", - "@opentui/solid": "catalog:", + "@opentui/core": "0.1.101", + "@opentui/solid": "0.1.101", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index a58ff05648..5da2740cce 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -1,7 +1,6 @@ import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import * as Clipboard from "@tui/util/clipboard" import * as Selection from "@tui/util/selection" -import * as Terminal from "@tui/util/terminal" import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core" import { RouteProvider, useRoute } from "@tui/context/route" import { @@ -120,12 +119,6 @@ export function tui(input: { const unguard = win32InstallCtrlCGuard() win32DisableProcessedInput() - const mode = await Terminal.getTerminalBackgroundColor() - - // Re-clear after getTerminalBackgroundColor() — setRawMode(false) restores - // the original console mode which re-enables ENABLE_PROCESSED_INPUT. - win32DisableProcessedInput() - const onExit = async () => { unguard?.() resolve() @@ -136,6 +129,7 @@ export function tui(input: { } const renderer = await createCliRenderer(rendererConfig(input.config)) + const mode = (await renderer.waitForThemeMode(1000)) ?? "dark" await render(() => { return ( diff --git a/packages/opencode/src/cli/cmd/tui/context/kv.tsx b/packages/opencode/src/cli/cmd/tui/context/kv.tsx index 803752e766..43266315bf 100644 --- a/packages/opencode/src/cli/cmd/tui/context/kv.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/kv.tsx @@ -1,7 +1,9 @@ import { Global } from "@/global" import { Filesystem } from "@/util" +import { Flock } from "@opencode-ai/shared/util/flock" +import { rename, rm } from "fs/promises" import { createSignal, type Setter } from "solid-js" -import { createStore } from "solid-js/store" +import { createStore, unwrap } from "solid-js/store" import { createSimpleContext } from "./helper" import path from "path" @@ -11,12 +13,29 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ const [ready, setReady] = createSignal(false) const [store, setStore] = createStore>() const filePath = path.join(Global.Path.state, "kv.json") + const lock = `tui-kv:${filePath}` + // Queue same-process writes so rapid updates persist in order. + let write = Promise.resolve() - Filesystem.readJson>(filePath) + // Write to a temp file first so kv.json is only replaced once the JSON is complete, avoiding partial writes if shutdown interrupts persistence. + function writeSnapshot(snapshot: Record) { + const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp` + return Filesystem.writeJson(tempPath, snapshot) + .then(() => rename(tempPath, filePath)) + .catch(async (error) => { + await rm(tempPath, { force: true }).catch(() => undefined) + throw error + }) + } + + // Read under the same lock used for writes because kv.json is shared across processes. + Flock.withLock(lock, () => Filesystem.readJson>(filePath)) .then((x) => { setStore(x) }) - .catch(() => {}) + .catch((error) => { + console.error("Failed to read KV state", { filePath, error }) + }) .finally(() => { setReady(true) }) @@ -44,7 +63,12 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ }, set(key: string, value: any) { setStore(key, value) - void Filesystem.writeJson(filePath, store) + const snapshot = structuredClone(unwrap(store)) + write = write + .then(() => Flock.withLock(lock, () => writeSnapshot(snapshot))) + .catch((error) => { + console.error("Failed to write KV state", { filePath, error }) + }) }, } return result diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 04670429da..af9582cfb0 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -314,8 +314,11 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ setStore( produce((draft) => { const lock = pick(kv.get("theme_mode_lock")) - const mode = pick(kv.get("theme_mode", props.mode)) - draft.mode = lock ?? mode ?? props.mode + const mode = lock ?? props.mode + if (!lock && pick(kv.get("theme_mode")) !== undefined) { + kv.set("theme_mode", undefined) + } + draft.mode = mode draft.lock = lock const active = config.theme ?? kv.get("theme", "opencode") draft.active = typeof active === "string" ? active : "opencode" @@ -373,7 +376,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ } function apply(mode: "dark" | "light") { - kv.set("theme_mode", mode) + if (store.lock !== undefined) kv.set("theme_mode", mode) if (store.mode === mode) return setStore("mode", mode) renderer.clearPaletteCache() @@ -389,6 +392,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ function free() { setStore("lock", undefined) kv.set("theme_mode_lock", undefined) + kv.set("theme_mode", undefined) const mode = renderer.themeMode if (mode) apply(mode) } @@ -397,7 +401,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ if (store.lock) return apply(mode) } - // renderer.on(CliRenderEvents.THEME_MODE, handle) + renderer.on(CliRenderEvents.THEME_MODE, handle) const refresh = () => { renderer.clearPaletteCache() diff --git a/packages/opencode/src/cli/cmd/tui/util/terminal.ts b/packages/opencode/src/cli/cmd/tui/util/terminal.ts index 46cf4635a7..c026b7381c 100644 --- a/packages/opencode/src/cli/cmd/tui/util/terminal.ts +++ b/packages/opencode/src/cli/cmd/tui/util/terminal.ts @@ -17,12 +17,6 @@ function parse(color: string): RGBA | null { return null } -function mode(bg: RGBA | null): "dark" | "light" { - if (!bg) return "dark" - const luminance = (0.299 * bg.r + 0.587 * bg.g + 0.114 * bg.b) / 255 - return luminance > 0.5 ? "light" : "dark" -} - /** * Query terminal colors including background, foreground, and palette (0-15). * Uses OSC escape sequences to retrieve actual terminal color values. @@ -100,36 +94,3 @@ export async function colors(): Promise<{ }, 1000) }) } - -// Keep startup mode detection separate from `colors()`: the TUI boot path only -// needs OSC 11 and should resolve on the first background response instead of -// waiting on the full palette query used by system theme generation. -export async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { - if (!process.stdin.isTTY) return "dark" - - return new Promise((resolve) => { - let timeout: NodeJS.Timeout - - const cleanup = () => { - process.stdin.setRawMode(false) - process.stdin.removeListener("data", handler) - clearTimeout(timeout) - } - - const handler = (data: Buffer) => { - const match = data.toString().match(/\x1b]11;([^\x07\x1b]+)/) - if (!match) return - cleanup() - resolve(mode(parse(match[1]))) - } - - process.stdin.setRawMode(true) - process.stdin.on("data", handler) - process.stdout.write("\x1b]11;?\x07") - - timeout = setTimeout(() => { - cleanup() - resolve("dark") - }, 1000) - }) -} diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 3beea3620b..c73addc478 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -22,8 +22,8 @@ "zod": "catalog:" }, "peerDependencies": { - "@opentui/core": ">=0.1.100", - "@opentui/solid": ">=0.1.100" + "@opentui/core": ">=0.1.101", + "@opentui/solid": ">=0.1.101" }, "peerDependenciesMeta": { "@opentui/core": { @@ -34,8 +34,8 @@ } }, "devDependencies": { - "@opentui/core": "catalog:", - "@opentui/solid": "catalog:", + "@opentui/core": "0.1.101", + "@opentui/solid": "0.1.101", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "typescript": "catalog:", From 9c7e52b8a17b154d6dc0f1004a70ff8214668c21 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 19 Apr 2026 22:52:42 +0000 Subject: [PATCH 228/335] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 042d0bb2e9..8997e133cd 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-i9TxYwWkJAR+kW6pbvhgQbRW9UYPtdrPQAGic4zPoa4=", - "aarch64-linux": "sha256-RYc/OYlETXUwkWBRDas+/P4cBW6zde4FqxxnMARu5vs=", - "aarch64-darwin": "sha256-jIhUOIRIQEa2WT62TVIedmRIhl/edhK8sbiAFvU3yCM=", - "x86_64-darwin": "sha256-xLGzaX7OofFlZzVgpORJR5QXD2u+54hp+t3cCfUtO84=" + "x86_64-linux": "sha256-YcVW8AGN3TP34CoBoCw+Fx30RL1aveNvxr5eoeOgYeg=", + "aarch64-linux": "sha256-G/J3YFfrpEwXSHa25kNyUhYpwPhzNIZf/4v+RCfuslk=", + "aarch64-darwin": "sha256-dNPYrGWKoafk4rHqc34U34TtiJGk87yUv5tKnliQcWs=", + "x86_64-darwin": "sha256-1LStvefCajGkbdXobMpk0IQyw9SQcQgGKE+U3Fc0Osw=" } } From 6eddf0824478461908c166318509eea0af363f09 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 19 Apr 2026 19:34:44 -0400 Subject: [PATCH 229/335] flip toolcall prune defaults --- packages/opencode/src/session/compaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 212f5fdbab..e927bdbe18 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -88,7 +88,7 @@ export const layer: Layer.Layer< // calls, then erases output of older tool calls to free context space const prune = Effect.fn("SessionCompaction.prune")(function* (input: { sessionID: SessionID }) { const cfg = yield* config.get() - if (cfg.compaction?.prune === false) return + if (!cfg.compaction?.prune) return log.info("pruning") const msgs = yield* session From 48acab48ad08e1d41bb3fe746855fba8d6b9a428 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 19 Apr 2026 19:47:29 -0400 Subject: [PATCH 230/335] ci: skip Docker builds during preview releases to save time --- packages/opencode/script/publish.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts index 7557c475f7..eb48524228 100755 --- a/packages/opencode/script/publish.ts +++ b/packages/opencode/script/publish.ts @@ -65,10 +65,10 @@ const image = "ghcr.io/anomalyco/opencode" const platforms = "linux/amd64,linux/arm64" const tags = [`${image}:${version}`, `${image}:${Script.channel}`] const tagFlags = tags.flatMap((t) => ["-t", t]) -await $`docker buildx build --platform ${platforms} ${tagFlags} --push .` // registries if (!Script.preview) { + await $`docker buildx build --platform ${platforms} ${tagFlags} --push .` // Calculate SHA values const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim()) const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim()) From 29f05cb1ee84b480f8ac862bf79e2864a97ee0f2 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 19 Apr 2026 23:48:44 +0000 Subject: [PATCH 231/335] chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 9 +++++++++ packages/sdk/openapi.json | 16 ++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 72a383a608..e1b0dbd576 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -885,6 +885,7 @@ export type CompactionPart = { type: "compaction" auto: boolean overflow?: boolean + tail_start_id?: string } export type Part = @@ -1642,6 +1643,14 @@ export type Config = { * Enable pruning of old tool outputs (default: true) */ prune?: boolean + /** + * Number of recent user turns, including their following assistant/tool responses, to keep verbatim during compaction (default: 2) + */ + tail_turns?: number + /** + * Token budget for retained recent turn spans during compaction + */ + tail_tokens?: number /** * Token buffer for compaction. Leaves enough window to avoid overflow during compaction. */ diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index b97d596b93..3ecb725d6d 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -9945,6 +9945,10 @@ }, "overflow": { "type": "boolean" + }, + "tail_start_id": { + "type": "string", + "pattern": "^msg.*" } }, "required": ["id", "sessionID", "messageID", "type", "auto"] @@ -11868,6 +11872,18 @@ "description": "Enable pruning of old tool outputs (default: true)", "type": "boolean" }, + "tail_turns": { + "description": "Number of recent user turns, including their following assistant/tool responses, to keep verbatim during compaction (default: 2)", + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "tail_tokens": { + "description": "Token budget for retained recent turn spans during compaction", + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, "reserved": { "description": "Token buffer for compaction. Leaves enough window to avoid overflow during compaction.", "type": "integer", From b9640fc7e472626e9b6806df800a34d48471ebb4 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 19 Apr 2026 19:53:43 -0400 Subject: [PATCH 232/335] core: fix session compaction test to properly enable prune config option --- .../opencode/test/session/compaction.test.ts | 149 +++++++++--------- 1 file changed, 78 insertions(+), 71 deletions(-) diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 015a1653a3..ef5fb5db03 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -564,65 +564,13 @@ describe("session.compaction.create", () => { describe("session.compaction.prune", () => { it.live( "compacts old completed tool output", - provideTmpdirInstance((dir) => - Effect.gen(function* () { - const compact = yield* SessionCompaction.Service - const ssn = yield* SessionNs.Service - const info = yield* ssn.create({}) - const a = yield* ssn.updateMessage({ - id: MessageID.ascending(), - role: "user", - sessionID: info.id, - agent: "build", - model: ref, - time: { created: Date.now() }, - }) - yield* ssn.updatePart({ - id: PartID.ascending(), - messageID: a.id, - sessionID: info.id, - type: "text", - text: "first", - }) - const b: MessageV2.Assistant = { - id: MessageID.ascending(), - role: "assistant", - sessionID: info.id, - mode: "build", - agent: "build", - path: { cwd: dir, root: dir }, - cost: 0, - tokens: { - output: 0, - input: 0, - reasoning: 0, - cache: { read: 0, write: 0 }, - }, - modelID: ref.modelID, - providerID: ref.providerID, - parentID: a.id, - time: { created: Date.now() }, - finish: "end_turn", - } - yield* ssn.updateMessage(b) - yield* ssn.updatePart({ - id: PartID.ascending(), - messageID: b.id, - sessionID: info.id, - type: "tool", - callID: crypto.randomUUID(), - tool: "bash", - state: { - status: "completed", - input: {}, - output: "x".repeat(200_000), - title: "done", - metadata: {}, - time: { start: Date.now(), end: Date.now() }, - }, - }) - for (const text of ["second", "third"]) { - const msg = yield* ssn.updateMessage({ + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const compact = yield* SessionCompaction.Service + const ssn = yield* SessionNs.Service + const info = yield* ssn.create({}) + const a = yield* ssn.updateMessage({ id: MessageID.ascending(), role: "user", sessionID: info.id, @@ -632,23 +580,82 @@ describe("session.compaction.prune", () => { }) yield* ssn.updatePart({ id: PartID.ascending(), - messageID: msg.id, + messageID: a.id, sessionID: info.id, type: "text", - text, + text: "first", }) - } + const b: MessageV2.Assistant = { + id: MessageID.ascending(), + role: "assistant", + sessionID: info.id, + mode: "build", + agent: "build", + path: { cwd: dir, root: dir }, + cost: 0, + tokens: { + output: 0, + input: 0, + reasoning: 0, + cache: { read: 0, write: 0 }, + }, + modelID: ref.modelID, + providerID: ref.providerID, + parentID: a.id, + time: { created: Date.now() }, + finish: "end_turn", + } + yield* ssn.updateMessage(b) + yield* ssn.updatePart({ + id: PartID.ascending(), + messageID: b.id, + sessionID: info.id, + type: "tool", + callID: crypto.randomUUID(), + tool: "bash", + state: { + status: "completed", + input: {}, + output: "x".repeat(200_000), + title: "done", + metadata: {}, + time: { start: Date.now(), end: Date.now() }, + }, + }) + for (const text of ["second", "third"]) { + const msg = yield* ssn.updateMessage({ + id: MessageID.ascending(), + role: "user", + sessionID: info.id, + agent: "build", + model: ref, + time: { created: Date.now() }, + }) + yield* ssn.updatePart({ + id: PartID.ascending(), + messageID: msg.id, + sessionID: info.id, + type: "text", + text, + }) + } - yield* compact.prune({ sessionID: info.id }) + yield* compact.prune({ sessionID: info.id }) - const msgs = yield* ssn.messages({ sessionID: info.id }) - const part = msgs.flatMap((msg) => msg.parts).find((part) => part.type === "tool") - expect(part?.type).toBe("tool") - expect(part?.state.status).toBe("completed") - if (part?.type === "tool" && part.state.status === "completed") { - expect(part.state.time.compacted).toBeNumber() - } - }), + const msgs = yield* ssn.messages({ sessionID: info.id }) + const part = msgs.flatMap((msg) => msg.parts).find((part) => part.type === "tool") + expect(part?.type).toBe("tool") + expect(part?.state.status).toBe("completed") + if (part?.type === "tool" && part.state.status === "completed") { + expect(part.state.time.compacted).toBeNumber() + } + }), + + { + config: { + compaction: { prune: true }, + }, + }, ), ) From e4be55792861504f23d055a37767357ee40b1d83 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 19 Apr 2026 20:01:30 -0400 Subject: [PATCH 233/335] ci: skip beta smoke fixes for now --- script/beta.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/script/beta.ts b/script/beta.ts index 6f4ff4ebf9..34d9dab1f8 100755 --- a/script/beta.ts +++ b/script/beta.ts @@ -295,10 +295,7 @@ async function main() { } if (applied.length > 0) { - const ok = await smoke(prs, applied) - if (!ok) { - throw new Error("Final smoke check failed") - } + console.log("\nSkipping final smoke check") } console.log("\nChecking if beta branch has changes...") From 6e0178655b94813762534b7642ba5174b45f022e Mon Sep 17 00:00:00 2001 From: Annie Surla <110621965+anniesurla@users.noreply.github.com> Date: Sun, 19 Apr 2026 17:57:49 -0700 Subject: [PATCH 234/335] feat(provider): add NVIDIA to popular providers, docs, and attribution headers (#22927) Co-authored-by: Kit Langton Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- packages/opencode/src/provider/provider.ts | 10 ++++ packages/web/src/content/docs/providers.mdx | 61 +++++++++++++++------ 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 8f6e1556ad..6e116fe41e 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -410,6 +410,16 @@ function custom(dep: CustomDep): Record { }, }, }), + nvidia: () => + Effect.succeed({ + autoload: false, + options: { + headers: { + "HTTP-Referer": "https://opencode.ai/", + "X-Title": "opencode", + }, + }, + }), vercel: () => Effect.succeed({ autoload: false, diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index bad9e1ebbc..030a50ba00 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -1316,30 +1316,57 @@ To use Kimi K2 from Moonshot AI: --- -### Mistral AI +### NVIDIA -1. Head over to the [Mistral AI console](https://console.mistral.ai/), create an account, and generate an API key. +NVIDIA provides access to Nemotron models and many other open models through [build.nvidia.com](https://build.nvidia.com) for free. -2. Run the `/connect` command and search for **Mistral AI**. +1. Head over to [build.nvidia.com](https://build.nvidia.com), create an account, and generate an API key. - ```txt - /connect - ``` +2. Run the `/connect` command and search for **NVIDIA**. -3. Enter your Mistral API key. + ```txt + /connect + ``` - ```txt - ┌ API key - │ - │ - └ enter - ``` +3. Enter your NVIDIA API key. -4. Run the `/models` command to select a model like _Mistral Medium_. + ```txt + ┌ API key + │ + │ + └ enter + ``` - ```txt - /models - ``` +4. Run the `/models` command to select a model like nemotron-3-super-120b-a12b. + + ```txt + /models + ``` + +#### On-Prem / NIM + +You can also use NVIDIA models locally via [NVIDIA NIM](https://docs.nvidia.com/nim/) by setting a custom base URL. + +```json title="opencode.json" {6} +{ + "$schema": "https://opencode.ai/config.json", + "provider": { + "nvidia": { + "options": { + "baseURL": "http://localhost:8000/v1" + } + } + } +} +``` + +#### Environment Variable + +Alternatively, set your API key as an environment variable. + +```bash frame="none" +export NVIDIA_API_KEY=nvapi-your-key-here +``` --- From a7a85c94b8705d038fdd5ee35320af453670a47d Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:39:15 +1000 Subject: [PATCH 235/335] fix(core): fix Windows managed install and bump ripgrep to 15.1.0 for ARM64 support (#23477) --- packages/opencode/src/file/ripgrep.ts | 38 ++++++++++++++------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index c84d9b522a..3e58d422f6 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -14,13 +14,14 @@ import { sanitizedProcessEnv } from "@/util/opencode-process" import { which } from "@/util/which" const log = Log.create({ service: "ripgrep" }) -const VERSION = "14.1.1" +const VERSION = "15.1.0" const PLATFORM = { "arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" }, "arm64-linux": { platform: "aarch64-unknown-linux-gnu", extension: "tar.gz" }, "x64-darwin": { platform: "x86_64-apple-darwin", extension: "tar.gz" }, "x64-linux": { platform: "x86_64-unknown-linux-musl", extension: "tar.gz" }, "arm64-win32": { platform: "aarch64-pc-windows-msvc", extension: "zip" }, + "ia32-win32": { platform: "i686-pc-windows-msvc", extension: "zip" }, "x64-win32": { platform: "x86_64-pc-windows-msvc", extension: "zip" }, } as const @@ -247,17 +248,20 @@ export const layer: Layer.Layer which("powershell.exe") ?? which("pwsh.exe"))) ?? "powershell.exe" const result = yield* run(shell, [ "-NoProfile", + "-NonInteractive", "-Command", - "Expand-Archive -LiteralPath $args[0] -DestinationPath $args[1] -Force", - archive, - dir, + `$global:ProgressPreference = 'SilentlyContinue'; Expand-Archive -LiteralPath '${archive.replaceAll("'", "''")}' -DestinationPath '${dir.replaceAll("'", "''")}' -Force`, ]) if (result.code !== 0) { return yield* Effect.fail(error(result.stderr || result.stdout, result.code)) @@ -271,12 +275,19 @@ export const layer: Layer.Layer which("rg")) + const system = yield* Effect.sync(() => which(process.platform === "win32" ? "rg.exe" : "rg")) if (system && (yield* fs.isFile(system).pipe(Effect.orDie))) return system const target = path.join(Global.Path.bin, `rg${process.platform === "win32" ? ".exe" : ""}`) @@ -304,17 +315,8 @@ export const layer: Layer.Layer Date: Mon, 20 Apr 2026 04:40:12 +0000 Subject: [PATCH 236/335] chore: generate --- packages/opencode/src/file/ripgrep.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index 3e58d422f6..d6fd61f1d0 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -275,7 +275,11 @@ export const layer: Layer.Layer Date: Mon, 20 Apr 2026 12:51:34 +0800 Subject: [PATCH 237/335] refactor(app): move QueryProvider to AppInterface (#23484) --- packages/app/src/app.tsx | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index dbe1074484..18c6fef30a 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -141,13 +141,11 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) { }> - - - - {props.children} - - - + + + {props.children} + + @@ -293,20 +291,22 @@ export function AppInterface(props: { > - - - {routerProps.children}} - > - - - - - - - - + + + + {routerProps.children}} + > + + + + + + + + + From 8bc4f91fd9952612bacf1297ca84424937ce9399 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 20 Apr 2026 00:14:21 -0500 Subject: [PATCH 238/335] fix: parallel edits sometimes would override each other (#23483) --- packages/opencode/src/tool/edit.ts | 128 +++++++++++++---------- packages/opencode/test/tool/edit.test.ts | 42 +++++--- 2 files changed, 97 insertions(+), 73 deletions(-) diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index f535183d4c..2f53cd1949 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -5,7 +5,7 @@ import z from "zod" import * as path from "path" -import { Effect } from "effect" +import { Effect, Semaphore } from "effect" import * as Tool from "./tool" import { LSP } from "../lsp" import { createTwoFilesPatch, diffLines } from "diff" @@ -32,6 +32,18 @@ function convertToLineEnding(text: string, ending: "\n" | "\r\n"): string { return text.replaceAll("\n", "\r\n") } +const locks = new Map() + +function lock(filePath: string) { + const resolvedFilePath = AppFileSystem.resolve(filePath) + const hit = locks.get(resolvedFilePath) + if (hit) return hit + + const next = Semaphore.makeUnsafe(1) + locks.set(resolvedFilePath, next) + return next +} + const Parameters = z.object({ filePath: z.string().describe("The absolute path to the file to modify"), oldString: z.string().describe("The text to replace"), @@ -68,11 +80,50 @@ export const EditTool = Tool.define( let diff = "" let contentOld = "" let contentNew = "" - yield* Effect.gen(function* () { - if (params.oldString === "") { - const existed = yield* afs.existsSafe(filePath) - contentNew = params.newString - diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew)) + yield* lock(filePath).withPermits(1)( + Effect.gen(function* () { + if (params.oldString === "") { + const existed = yield* afs.existsSafe(filePath) + contentNew = params.newString + diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew)) + yield* ctx.ask({ + permission: "edit", + patterns: [path.relative(Instance.worktree, filePath)], + always: ["*"], + metadata: { + filepath: filePath, + diff, + }, + }) + yield* afs.writeWithDirs(filePath, params.newString) + yield* format.file(filePath) + yield* bus.publish(File.Event.Edited, { file: filePath }) + yield* bus.publish(FileWatcher.Event.Updated, { + file: filePath, + event: existed ? "change" : "add", + }) + return + } + + const info = yield* afs.stat(filePath).pipe(Effect.catch(() => Effect.succeed(undefined))) + if (!info) throw new Error(`File ${filePath} not found`) + if (info.type === "Directory") throw new Error(`Path is a directory, not a file: ${filePath}`) + contentOld = yield* afs.readFileString(filePath) + + const ending = detectLineEnding(contentOld) + const old = convertToLineEnding(normalizeLineEndings(params.oldString), ending) + const next = convertToLineEnding(normalizeLineEndings(params.newString), ending) + + contentNew = replace(contentOld, old, next, params.replaceAll) + + diff = trimDiff( + createTwoFilesPatch( + filePath, + filePath, + normalizeLineEndings(contentOld), + normalizeLineEndings(contentNew), + ), + ) yield* ctx.ask({ permission: "edit", patterns: [path.relative(Instance.worktree, filePath)], @@ -82,62 +133,25 @@ export const EditTool = Tool.define( diff, }, }) - yield* afs.writeWithDirs(filePath, params.newString) + + yield* afs.writeWithDirs(filePath, contentNew) yield* format.file(filePath) yield* bus.publish(File.Event.Edited, { file: filePath }) yield* bus.publish(FileWatcher.Event.Updated, { file: filePath, - event: existed ? "change" : "add", + event: "change", }) - return - } - - const info = yield* afs.stat(filePath).pipe(Effect.catch(() => Effect.succeed(undefined))) - if (!info) throw new Error(`File ${filePath} not found`) - if (info.type === "Directory") throw new Error(`Path is a directory, not a file: ${filePath}`) - contentOld = yield* afs.readFileString(filePath) - - const ending = detectLineEnding(contentOld) - const old = convertToLineEnding(normalizeLineEndings(params.oldString), ending) - const next = convertToLineEnding(normalizeLineEndings(params.newString), ending) - - contentNew = replace(contentOld, old, next, params.replaceAll) - - diff = trimDiff( - createTwoFilesPatch( - filePath, - filePath, - normalizeLineEndings(contentOld), - normalizeLineEndings(contentNew), - ), - ) - yield* ctx.ask({ - permission: "edit", - patterns: [path.relative(Instance.worktree, filePath)], - always: ["*"], - metadata: { - filepath: filePath, - diff, - }, - }) - - yield* afs.writeWithDirs(filePath, contentNew) - yield* format.file(filePath) - yield* bus.publish(File.Event.Edited, { file: filePath }) - yield* bus.publish(FileWatcher.Event.Updated, { - file: filePath, - event: "change", - }) - contentNew = yield* afs.readFileString(filePath) - diff = trimDiff( - createTwoFilesPatch( - filePath, - filePath, - normalizeLineEndings(contentOld), - normalizeLineEndings(contentNew), - ), - ) - }).pipe(Effect.orDie) + contentNew = yield* afs.readFileString(filePath) + diff = trimDiff( + createTwoFilesPatch( + filePath, + filePath, + normalizeLineEndings(contentOld), + normalizeLineEndings(contentNew), + ), + ) + }).pipe(Effect.orDie), + ) const filediff: Snapshot.FileDiff = { file: filePath, diff --git a/packages/opencode/test/tool/edit.test.ts b/packages/opencode/test/tool/edit.test.ts index 4759b8be36..8756d65d24 100644 --- a/packages/opencode/test/tool/edit.test.ts +++ b/packages/opencode/test/tool/edit.test.ts @@ -29,11 +29,6 @@ afterEach(async () => { await Instance.disposeAll() }) -async function touch(file: string, time: number) { - const date = new Date(time) - await fs.utimes(file, date, date) -} - const runtime = ManagedRuntime.make( Layer.mergeAll( LSP.defaultLayer, @@ -639,44 +634,59 @@ describe("tool.edit", () => { }) describe("concurrent editing", () => { - test("serializes concurrent edits to same file", async () => { + test("preserves concurrent edits to different sections of the same file", async () => { await using tmp = await tmpdir() const filepath = path.join(tmp.path, "file.txt") - await fs.writeFile(filepath, "0", "utf-8") + await fs.writeFile(filepath, "top = 0\nmiddle = keep\nbottom = 0\n", "utf-8") await Instance.provide({ directory: tmp.path, fn: async () => { const edit = await resolve() + let asks = 0 + const firstAsk = Promise.withResolvers() + const delayedCtx = { + ...ctx, + ask: () => + Effect.gen(function* () { + asks++ + if (asks !== 1) return + firstAsk.resolve() + yield* Effect.promise(() => Bun.sleep(50)) + }), + } - // Two concurrent edits const promise1 = Effect.runPromise( edit.execute( { filePath: filepath, - oldString: "0", - newString: "1", + oldString: "top = 0", + newString: "top = 1", }, - ctx, + delayedCtx, ), ) + await firstAsk.promise + const promise2 = Effect.runPromise( edit.execute( { filePath: filepath, - oldString: "0", - newString: "2", + oldString: "bottom = 0", + newString: "bottom = 2", }, - ctx, + delayedCtx, ), ) - // Both should complete without error (though one might fail due to content mismatch) const results = await Promise.allSettled([promise1, promise2]) - expect(results.some((r) => r.status === "fulfilled")).toBe(true) + expect(results[0]?.status).toBe("fulfilled") + expect(results[1]?.status).toBe("fulfilled") + expect(await fs.readFile(filepath, "utf-8")).toBe("top = 1\nmiddle = keep\nbottom = 2\n") }, }) }) + }) }) From 84e322b0fdb178ad420f3f6507a22c0da590f524 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 20 Apr 2026 05:15:29 +0000 Subject: [PATCH 239/335] chore: generate --- packages/opencode/test/tool/edit.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/opencode/test/tool/edit.test.ts b/packages/opencode/test/tool/edit.test.ts index 8756d65d24..b5fbc0a67d 100644 --- a/packages/opencode/test/tool/edit.test.ts +++ b/packages/opencode/test/tool/edit.test.ts @@ -687,6 +687,5 @@ describe("tool.edit", () => { }, }) }) - }) }) From 687b7588820df02dfe7397a399f213f394aa6b09 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:17:37 +0800 Subject: [PATCH 240/335] app: better loading (#23489) --- packages/app/src/components/prompt-input.tsx | 41 ++--- packages/app/src/context/global-sync.tsx | 24 +-- .../app/src/context/global-sync/bootstrap.ts | 144 ++++++++++-------- .../src/context/global-sync/child-store.ts | 10 +- packages/app/src/context/prompt.tsx | 6 +- packages/app/src/index.css | 9 ++ packages/app/src/pages/directory-layout.tsx | 11 +- packages/app/src/pages/layout/helpers.ts | 2 +- .../src/pages/layout/sidebar-workspace.tsx | 2 +- 9 files changed, 142 insertions(+), 107 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 156b0b3a4a..1131baa498 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1,6 +1,6 @@ import { useFilteredList } from "@opencode-ai/ui/hooks" import { useSpring } from "@opencode-ai/ui/motion-spring" -import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal } from "solid-js" +import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal, createResource } from "solid-js" import { createStore } from "solid-js/store" import { useLocal } from "@/context/local" import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file" @@ -54,7 +54,7 @@ import { PromptImageAttachments } from "./prompt-input/image-attachments" import { PromptDragOverlay } from "./prompt-input/drag-overlay" import { promptPlaceholder } from "./prompt-input/placeholder" import { ImagePreview } from "@opencode-ai/ui/image-preview" -import { useQuery } from "@tanstack/solid-query" +import { useQueries, useQuery } from "@tanstack/solid-query" import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap" interface PromptInputProps { @@ -1252,16 +1252,21 @@ export const PromptInput: Component = (props) => { } } - const agentsQuery = useQuery(() => loadAgentsQuery(sdk.directory)) + const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({ + queries: [loadAgentsQuery(sdk.directory), loadProvidersQuery(null), loadProvidersQuery(sdk.directory)], + })) + const agentsLoading = () => agentsQuery.isLoading - - const globalProvidersQuery = useQuery(() => loadProvidersQuery(null)) - const providersQuery = useQuery(() => loadProvidersQuery(sdk.directory)) - const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading + const [promptReady] = createResource( + () => prompt.ready().promise, + (p) => p, + ) + return (
+ {(promptReady(), null)} (slashPopoverRef = el)} @@ -1358,15 +1363,13 @@ export const PromptInput: Component = (props) => { }} style={{ "padding-bottom": space }} /> - -
- {placeholder()} -
-
+
+ {placeholder()} +
= (props) => {
-
+
= (props) => { -
+
0} fallback={ @@ -1554,7 +1557,7 @@ export const PromptInput: Component = (props) => {
-
+
{ + setStore( + "sessionTotal", + estimateRootSessionTotal({ + count: nonArchived.length, + limit: x.limit, + limited: x.limited, + }), + ) + setStore("session", reconcile(sessions, { key: "id" })) + cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo) + }) sessionMeta.set(directory, { limit }) }) .catch((err) => { diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 17fe726f90..be789a5e53 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -19,7 +19,6 @@ import type { State, VcsCache } from "./types" import { cmp, normalizeAgentList, normalizeProviderList } from "./utils" import { formatServerError } from "@/utils/server-errors" import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query" -import { loadSessionsQuery } from "../global-sync" type GlobalStore = { ready: boolean @@ -82,6 +81,9 @@ export async function bootstrapGlobal(input: { input.setGlobalStore("config", x.data!) }), ), + ] + + const slow = [ () => input.queryClient.fetchQuery({ ...loadProvidersQuery(null), @@ -93,9 +95,6 @@ export async function bootstrapGlobal(input: { }), ), }), - ] - - const slow = [ () => retry(() => input.globalSDK.path.get().then((x) => { @@ -183,8 +182,43 @@ function warmSessions(input: { export const loadProvidersQuery = (directory: string | null) => queryOptions({ queryKey: [directory, "providers"], queryFn: skipToken }) -export const loadAgentsQuery = (directory: string | null) => - queryOptions({ queryKey: [directory, "agents"], queryFn: skipToken }) +export const loadAgentsQuery = ( + directory: string | null, + sdk?: OpencodeClient, + transform?: (x: Awaited>) => void, +) => + queryOptions({ + queryKey: [directory, "agents"], + queryFn: + sdk && transform + ? () => + retry(() => + sdk.app + .agents() + .then(transform) + .then(() => null), + ) + : skipToken, + }) + +export const loadPathQuery = ( + directory: string | null, + sdk?: OpencodeClient, + transform?: (x: Awaited>) => void, +) => + queryOptions({ + queryKey: [directory, "path"], + queryFn: + sdk && transform + ? () => + retry(() => + sdk.path.get().then(async (x) => { + transform(x) + return x.data! + }), + ) + : skipToken, + }) export async function bootstrapDirectory(input: { directory: string @@ -222,45 +256,27 @@ export async function bootstrapDirectory(input: { input.setStore("lsp", []) if (loading) input.setStore("status", "partial") - const fast = [() => Promise.resolve(input.loadSessions(input.directory))] - - const errs = errors(await runAll(fast)) - if (errs.length > 0) { - console.error("Failed to bootstrap instance", errs[0]) - const project = getFilename(input.directory) - showToast({ - variant: "error", - title: input.translate("toast.project.reloadFailed.title", { project }), - description: formatServerError(errs[0], input.translate), - }) - } - + const rev = (providerRev.get(input.directory) ?? 0) + 1 + providerRev.set(input.directory, rev) ;(async () => { const slow = [ + () => Promise.resolve(input.loadSessions(input.directory)), () => - input.queryClient.ensureQueryData({ - ...loadAgentsQuery(input.directory), - queryFn: () => - retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))).then( - () => null, - ), - }), + input.queryClient.ensureQueryData( + loadAgentsQuery(input.directory, input.sdk, (x) => input.setStore("agent", normalizeAgentList(x.data))), + ), () => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))), () => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))), - () => - seededProject - ? Promise.resolve() - : retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)), - () => - seededPath - ? Promise.resolve() - : retry(() => - input.sdk.path.get().then((x) => { - input.setStore("path", x.data!) - const next = projectID(x.data?.directory ?? input.directory, input.global.project) - if (next) input.setStore("project", next) - }), - ), + !seededProject && + (() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))), + !seededPath && + (() => + input.queryClient.ensureQueryData( + loadPathQuery(input.directory, input.sdk, (x) => { + const next = projectID(x.data?.directory ?? input.directory, input.global.project) + if (next) input.setStore("project", next) + }), + )), () => retry(() => input.sdk.vcs.get().then((x) => { @@ -330,7 +346,28 @@ export async function bootstrapDirectory(input: { input.setStore("mcp_ready", true) }), ), - ] + () => + input.queryClient.ensureQueryData({ + ...loadProvidersQuery(input.directory), + queryFn: () => + retry(() => input.sdk.provider.list()) + .then((x) => { + if (providerRev.get(input.directory) !== rev) return + input.setStore("provider", normalizeProviderList(x.data!)) + input.setStore("provider_ready", true) + }) + .catch((err) => { + if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err) + const project = getFilename(input.directory) + showToast({ + variant: "error", + title: input.translate("toast.project.reloadFailed.title", { project }), + description: formatServerError(err, input.translate), + }) + }) + .then(() => null), + }), + ].filter(Boolean) as (() => Promise)[] await waitForPaint() const slowErrs = errors(await runAll(slow)) @@ -344,29 +381,6 @@ export async function bootstrapDirectory(input: { }) } - if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete") - - const rev = (providerRev.get(input.directory) ?? 0) + 1 - providerRev.set(input.directory, rev) - void input.queryClient.ensureQueryData({ - ...loadSessionsQuery(input.directory), - queryFn: () => - retry(() => input.sdk.provider.list()) - .then((x) => { - if (providerRev.get(input.directory) !== rev) return - input.setStore("provider", normalizeProviderList(x.data!)) - input.setStore("provider_ready", true) - }) - .catch((err) => { - if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err) - const project = getFilename(input.directory) - showToast({ - variant: "error", - title: input.translate("toast.project.reloadFailed.title", { project }), - description: formatServerError(err, input.translate), - }) - }) - .then(() => null), - }) + if (loading && slowErrs.length === 0) input.setStore("status", "complete") })() } diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index 3fe67e4fbe..c92d2ae570 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -14,6 +14,8 @@ import { type VcsCache, } from "./types" import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction" +import { useQuery } from "@tanstack/solid-query" +import { loadPathQuery } from "./bootstrap" export function createChildStoreManager(input: { owner: Owner @@ -156,6 +158,8 @@ export function createChildStoreManager(input: { createRoot((dispose) => { const initialMeta = meta[0].value const initialIcon = icon[0].value + + const pathQuery = useQuery(() => loadPathQuery(directory)) const child = createStore({ project: "", projectMeta: initialMeta, @@ -163,7 +167,11 @@ export function createChildStoreManager(input: { provider_ready: false, provider: { all: [], connected: [], default: {} }, config: {}, - path: { state: "", config: "", worktree: "", directory: "", home: "" }, + get path() { + if (pathQuery.isLoading || !pathQuery.data) + return { state: "", config: "", worktree: "", directory: "", home: "" } + return pathQuery.data + }, status: "loading" as const, agent: [], command: [], diff --git a/packages/app/src/context/prompt.tsx b/packages/app/src/context/prompt.tsx index 9b666e5e75..15af57b355 100644 --- a/packages/app/src/context/prompt.tsx +++ b/packages/app/src/context/prompt.tsx @@ -185,9 +185,9 @@ function createPromptSession(dir: string, id: string | undefined) { return { ready, - current: createMemo(() => store.prompt), + current: () => store.prompt, cursor: createMemo(() => store.cursor), - dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)), + dirty: () => !isPromptEqual(store.prompt, DEFAULT_PROMPT), context: { items: createMemo(() => store.context.items), add(item: ContextItem) { @@ -277,7 +277,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext( const pick = (scope?: Scope) => (scope ? load(scope.dir, scope.id) : session()) return { - ready: () => session().ready(), + ready: () => session().ready, current: () => session().current(), cursor: () => session().cursor(), dirty: () => session().dirty(), diff --git a/packages/app/src/index.css b/packages/app/src/index.css index 9fbc5be706..8db576dd83 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -73,4 +73,13 @@ width: auto; } } + + @keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } } diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index f604dd6c5c..36514f56c6 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -2,7 +2,7 @@ import { DataProvider } from "@opencode-ai/ui/context" import { showToast } from "@opencode-ai/ui/toast" import { base64Encode } from "@opencode-ai/shared/util/encode" import { useLocation, useNavigate, useParams } from "@solidjs/router" -import { createEffect, createMemo, type ParentProps, Show } from "solid-js" +import { createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js" import { useLanguage } from "@/context/language" import { LocalProvider } from "@/context/local" import { SDKProvider } from "@/context/sdk" @@ -23,11 +23,10 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) { navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true }) }) - createEffect(() => { - const id = params.id - if (!id) return - void sync.session.sync(id) - }) + createResource( + () => params.id, + (id) => sync.session.sync(id), + ) return ( workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived -const roots = (store: SessionStore) => +export const roots = (store: SessionStore) => (store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory)) export const sortedRootSessions = (store: SessionStore, now: number) => roots(store).sort(sortSessions(now)) diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index 0202cfc3be..cbb5701065 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -321,7 +321,7 @@ export const SortableWorkspace = (props: { const hasMore = createMemo(() => workspaceStore.sessionTotal > count()) const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) })) const busy = createMemo(() => props.ctx.isBusy(props.directory)) - const loading = () => query.isLoading + const loading = () => query.isLoading && count() === 0 const touch = createMediaQuery("(hover: none)") const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id))) const loadMore = async () => { From e539efe2b9a627927ffcb1420098a69038d042e2 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 20 Apr 2026 00:49:46 -0500 Subject: [PATCH 241/335] fix: patch arborist to get around bun bug (#23460) --- bun.lock | 4 +++ package.json | 1 + packages/opencode/package.json | 1 + packages/opencode/src/npm/config.ts | 0 packages/opencode/src/npmcli-config.d.ts | 43 ++++++++++++++++++++++++ packages/shared/src/types.d.ts | 2 ++ patches/@npmcli%2Fagent@4.0.0.patch | 13 +++++++ 7 files changed, 64 insertions(+) create mode 100644 packages/opencode/src/npm/config.ts create mode 100644 packages/opencode/src/npmcli-config.d.ts create mode 100644 patches/@npmcli%2Fagent@4.0.0.patch diff --git a/bun.lock b/bun.lock index fc96627961..6dea120fa8 100644 --- a/bun.lock +++ b/bun.lock @@ -354,6 +354,7 @@ "@lydell/node-pty": "catalog:", "@modelcontextprotocol/sdk": "1.27.1", "@npmcli/arborist": "9.4.0", + "@npmcli/config": "10.8.1", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", @@ -660,6 +661,7 @@ "patchedDependencies": { "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + "@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch", }, "overrides": { "@types/bun": "catalog:", @@ -1468,6 +1470,8 @@ "@npmcli/arborist": ["@npmcli/arborist@9.4.0", "", { "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/fs": "^5.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/map-workspaces": "^5.0.0", "@npmcli/metavuln-calculator": "^9.0.2", "@npmcli/name-from-folder": "^4.0.0", "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/query": "^5.0.0", "@npmcli/redact": "^4.0.0", "@npmcli/run-script": "^10.0.0", "bin-links": "^6.0.0", "cacache": "^20.0.1", "common-ancestor-path": "^2.0.0", "hosted-git-info": "^9.0.0", "json-stringify-nice": "^1.1.4", "lru-cache": "^11.2.1", "minimatch": "^10.0.3", "nopt": "^9.0.0", "npm-install-checks": "^8.0.0", "npm-package-arg": "^13.0.0", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "pacote": "^21.0.2", "parse-conflict-json": "^5.0.1", "proc-log": "^6.0.0", "proggy": "^4.0.0", "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^3.0.1", "semver": "^7.3.7", "ssri": "^13.0.0", "treeverse": "^3.0.0", "walk-up-path": "^4.0.0" }, "bin": { "arborist": "bin/index.js" } }, "sha512-4Bm8hNixJG/sii1PMnag0V9i/sGOX9VRzFrUiZMSBJpGlLR38f+Btl85d07G9GL56xO0l0OZjvrGNYsDYp0xKA=="], + "@npmcli/config": ["@npmcli/config@10.8.1", "", { "dependencies": { "@npmcli/map-workspaces": "^5.0.0", "@npmcli/package-json": "^7.0.0", "ci-info": "^4.0.0", "ini": "^6.0.0", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "walk-up-path": "^4.0.0" } }, "sha512-MAYk9IlIGiyC0c9fnjdBSQfIFPZT0g1MfeSiD1UXTq2zJOLX55jS9/sETJHqw/7LN18JjITrhYfgCfapbmZHiQ=="], + "@npmcli/fs": ["@npmcli/fs@5.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og=="], "@npmcli/git": ["@npmcli/git@7.0.2", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/promise-spawn": "^9.0.0", "ini": "^6.0.0", "lru-cache": "^11.2.1", "npm-pick-manifest": "^11.0.1", "proc-log": "^6.0.0", "semver": "^7.3.5", "which": "^6.0.0" } }, "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg=="], diff --git a/package.json b/package.json index 063226ad0c..06bf9c91ae 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,7 @@ "@types/node": "catalog:" }, "patchedDependencies": { + "@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch" } diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 42f30b45ef..4644922fc3 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -110,6 +110,7 @@ "@lydell/node-pty": "catalog:", "@modelcontextprotocol/sdk": "1.27.1", "@npmcli/arborist": "9.4.0", + "@npmcli/config": "10.8.1", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", diff --git a/packages/opencode/src/npm/config.ts b/packages/opencode/src/npm/config.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/opencode/src/npmcli-config.d.ts b/packages/opencode/src/npmcli-config.d.ts new file mode 100644 index 0000000000..c9b20517ad --- /dev/null +++ b/packages/opencode/src/npmcli-config.d.ts @@ -0,0 +1,43 @@ +declare module "@npmcli/config" { + type Data = Record + type Where = "default" | "builtin" | "global" | "user" | "project" | "env" | "cli" + + namespace Config { + interface Options { + definitions: Data + shorthands: Record + npmPath: string + flatten?: (input: Data, flat?: Data) => Data + nerfDarts?: string[] + argv?: string[] + cwd?: string + env?: NodeJS.ProcessEnv + execPath?: string + platform?: NodeJS.Platform + warn?: boolean + } + } + + class Config { + constructor(input: Config.Options) + + readonly data: Map + readonly flat: Data + + load(): Promise + } + + export = Config +} + +declare module "@npmcli/config/lib/definitions" { + export const definitions: Record + export const shorthands: Record + export const flatten: (input: Record, flat?: Record) => Record + export const nerfDarts: string[] + export const proxyEnv: string[] +} + +declare module "@npmcli/config/lib/definitions/index.js" { + export * from "@npmcli/config/lib/definitions" +} diff --git a/packages/shared/src/types.d.ts b/packages/shared/src/types.d.ts index b5d667f1d9..60e1639adb 100644 --- a/packages/shared/src/types.d.ts +++ b/packages/shared/src/types.d.ts @@ -5,6 +5,7 @@ declare module "@npmcli/arborist" { progress?: boolean savePrefix?: string ignoreScripts?: boolean + [key: string]: unknown } export interface ArboristNode { @@ -24,6 +25,7 @@ declare module "@npmcli/arborist" { add?: string[] save?: boolean saveType?: "prod" | "dev" | "optional" | "peer" + [key: string]: unknown } export class Arborist { diff --git a/patches/@npmcli%2Fagent@4.0.0.patch b/patches/@npmcli%2Fagent@4.0.0.patch new file mode 100644 index 0000000000..a3506a90e3 --- /dev/null +++ b/patches/@npmcli%2Fagent@4.0.0.patch @@ -0,0 +1,13 @@ +diff --git a/lib/agents.js b/lib/agents.js +index 45ec59c4c13757379095131c4f0a5ea6f7284f45..0763b031e355a755ec6a26f98461aa3f63b8339b 100644 +--- a/lib/agents.js ++++ b/lib/agents.js +@@ -32,7 +32,7 @@ module.exports = class Agent extends AgentBase { + } + + get proxy () { +- return this.#proxy ? { url: this.#proxy } : {} ++ return this.#proxy ? { url: this.#proxy.toString() } : {} + } + + #getProxy (options) { From c6c56ac2cfc3683d963ef33be18809165b25ca68 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 20 Apr 2026 01:06:29 -0500 Subject: [PATCH 242/335] tweak: rename tail_tokens -> preserve_recent_tokens (#23491) --- packages/opencode/src/config/config.ts | 4 ++-- packages/opencode/src/session/compaction.ts | 15 +++++++++------ .../opencode/test/session/compaction.test.ts | 16 ++++++++-------- packages/sdk/js/src/v2/gen/types.gen.ts | 4 ++-- packages/sdk/openapi.json | 4 ++-- 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 2b0c97eae9..55684fc70d 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -208,8 +208,8 @@ const InfoSchema = Schema.Struct({ description: "Number of recent user turns, including their following assistant/tool responses, to keep verbatim during compaction (default: 2)", }), - tail_tokens: Schema.optional(NonNegativeInt).annotate({ - description: "Token budget for retained recent turn spans during compaction", + preserve_recent_tokens: Schema.optional(NonNegativeInt).annotate({ + description: "Maximum number of tokens from recent turns to preserve verbatim after compaction", }), reserved: Schema.optional(NonNegativeInt).annotate({ description: "Token buffer for compaction. Leaves enough window to avoid overflow during compaction.", diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 8da6842397..b6d555afd5 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -34,18 +34,21 @@ export const PRUNE_MINIMUM = 20_000 export const PRUNE_PROTECT = 40_000 const PRUNE_PROTECTED_TOOLS = ["skill"] const DEFAULT_TAIL_TURNS = 2 -const MIN_TAIL_TOKENS = 2_000 -const MAX_TAIL_TOKENS = 8_000 +const MIN_PRESERVE_RECENT_TOKENS = 2_000 +const MAX_PRESERVE_RECENT_TOKENS = 8_000 type Turn = { start: number end: number id: MessageID } -function tailBudget(input: { cfg: Config.Info; model: Provider.Model }) { +function preserveRecentBudget(input: { cfg: Config.Info; model: Provider.Model }) { return ( - input.cfg.compaction?.tail_tokens ?? - Math.min(MAX_TAIL_TOKENS, Math.max(MIN_TAIL_TOKENS, Math.floor(usable(input) * 0.25))) + input.cfg.compaction?.preserve_recent_tokens ?? + Math.min( + MAX_PRESERVE_RECENT_TOKENS, + Math.max(MIN_PRESERVE_RECENT_TOKENS, Math.floor(usable(input) * 0.25)), + ) ) } @@ -134,7 +137,7 @@ export const layer: Layer.Layer< }) { const limit = input.cfg.compaction?.tail_turns ?? DEFAULT_TAIL_TURNS if (limit <= 0) return { head: input.messages, tail_start_id: undefined } - const budget = tailBudget({ cfg: input.cfg, model: input.model }) + const budget = preserveRecentBudget({ cfg: input.cfg, model: input.model }) const all = turns(input.messages) if (!all.length) return { head: input.messages, tail_start_id: undefined } const recent = all.slice(-limit) diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index ef5fb5db03..96c8e4ae8b 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -925,7 +925,7 @@ describe("session.compaction.process", () => { auto: false, }) - const rt = runtime("continue", Plugin.defaultLayer, wide(), cfg({ tail_turns: 2, tail_tokens: 10_000 })) + const rt = runtime("continue", Plugin.defaultLayer, wide(), cfg({ tail_turns: 2, preserve_recent_tokens: 10_000 })) try { const msgs = await svc.messages({ sessionID: session.id }) const parent = msgs.at(-1)?.info.id @@ -954,7 +954,7 @@ describe("session.compaction.process", () => { }) }) - test("shrinks retained tail to fit tail token budget", async () => { + test("shrinks retained tail to fit preserve token budget", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, @@ -970,7 +970,7 @@ describe("session.compaction.process", () => { auto: false, }) - const rt = runtime("continue", Plugin.defaultLayer, wide(), cfg({ tail_turns: 2, tail_tokens: 100 })) + const rt = runtime("continue", Plugin.defaultLayer, wide(), cfg({ tail_turns: 2, preserve_recent_tokens: 100 })) try { const msgs = await svc.messages({ sessionID: session.id }) const parent = msgs.at(-1)?.info.id @@ -999,7 +999,7 @@ describe("session.compaction.process", () => { }) }) - test("falls back to full summary when even one recent turn exceeds tail budget", async () => { + test("falls back to full summary when even one recent turn exceeds preserve token budget", async () => { await using tmp = await tmpdir({ git: true }) const stub = llm() let captured = "" @@ -1021,7 +1021,7 @@ describe("session.compaction.process", () => { auto: false, }) - const rt = liveRuntime(stub.layer, wide(), cfg({ tail_turns: 1, tail_tokens: 20 })) + const rt = liveRuntime(stub.layer, wide(), cfg({ tail_turns: 1, preserve_recent_tokens: 20 })) try { const msgs = await svc.messages({ sessionID: session.id }) const parent = msgs.at(-1)?.info.id @@ -1051,7 +1051,7 @@ describe("session.compaction.process", () => { }) }) - test("falls back to full summary when retained tail media exceeds tail budget", async () => { + test("falls back to full summary when retained tail media exceeds preserve token budget", async () => { await using tmp = await tmpdir({ git: true }) const stub = llm() let captured = "" @@ -1082,7 +1082,7 @@ describe("session.compaction.process", () => { auto: false, }) - const rt = liveRuntime(stub.layer, wide(), cfg({ tail_turns: 1, tail_tokens: 100 })) + const rt = liveRuntime(stub.layer, wide(), cfg({ tail_turns: 1, preserve_recent_tokens: 100 })) try { const msgs = await svc.messages({ sessionID: session.id }) const parent = msgs.at(-1)?.info.id @@ -1544,7 +1544,7 @@ describe("session.compaction.process", () => { auto: false, }) - const rt = liveRuntime(stub.layer, wide(), cfg({ tail_turns: 2, tail_tokens: 10_000 })) + const rt = liveRuntime(stub.layer, wide(), cfg({ tail_turns: 2, preserve_recent_tokens: 10_000 })) try { let msgs = await svc.messages({ sessionID: session.id }) let parent = msgs.at(-1)?.info.id diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index e1b0dbd576..d14fab1919 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1648,9 +1648,9 @@ export type Config = { */ tail_turns?: number /** - * Token budget for retained recent turn spans during compaction + * Maximum number of tokens from recent turns to preserve verbatim after compaction */ - tail_tokens?: number + preserve_recent_tokens?: number /** * Token buffer for compaction. Leaves enough window to avoid overflow during compaction. */ diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 3ecb725d6d..dbd85874fc 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -11878,8 +11878,8 @@ "minimum": 0, "maximum": 9007199254740991 }, - "tail_tokens": { - "description": "Token budget for retained recent turn spans during compaction", + "preserve_recent_tokens": { + "description": "Maximum number of tokens from recent turns to preserve verbatim after compaction", "type": "integer", "minimum": 0, "maximum": 9007199254740991 From f3d5a71620d0ffe399490cdbb0bb113524debdf8 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 20 Apr 2026 06:07:28 +0000 Subject: [PATCH 243/335] chore: generate --- packages/opencode/src/session/compaction.ts | 5 +---- packages/opencode/test/session/compaction.test.ts | 7 ++++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index b6d555afd5..037543064e 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -45,10 +45,7 @@ type Turn = { function preserveRecentBudget(input: { cfg: Config.Info; model: Provider.Model }) { return ( input.cfg.compaction?.preserve_recent_tokens ?? - Math.min( - MAX_PRESERVE_RECENT_TOKENS, - Math.max(MIN_PRESERVE_RECENT_TOKENS, Math.floor(usable(input) * 0.25)), - ) + Math.min(MAX_PRESERVE_RECENT_TOKENS, Math.max(MIN_PRESERVE_RECENT_TOKENS, Math.floor(usable(input) * 0.25))) ) } diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 96c8e4ae8b..14b47922b4 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -925,7 +925,12 @@ describe("session.compaction.process", () => { auto: false, }) - const rt = runtime("continue", Plugin.defaultLayer, wide(), cfg({ tail_turns: 2, preserve_recent_tokens: 10_000 })) + const rt = runtime( + "continue", + Plugin.defaultLayer, + wide(), + cfg({ tail_turns: 2, preserve_recent_tokens: 10_000 }), + ) try { const msgs = await svc.messages({ sessionID: session.id }) const parent = msgs.at(-1)?.info.id From 3ddc69ec2bd7c0f7108160754d4cd472b37e9d24 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 20 Apr 2026 06:36:01 +0000 Subject: [PATCH 244/335] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 8997e133cd..e68adae5ba 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-YcVW8AGN3TP34CoBoCw+Fx30RL1aveNvxr5eoeOgYeg=", - "aarch64-linux": "sha256-G/J3YFfrpEwXSHa25kNyUhYpwPhzNIZf/4v+RCfuslk=", - "aarch64-darwin": "sha256-dNPYrGWKoafk4rHqc34U34TtiJGk87yUv5tKnliQcWs=", - "x86_64-darwin": "sha256-1LStvefCajGkbdXobMpk0IQyw9SQcQgGKE+U3Fc0Osw=" + "x86_64-linux": "sha256-DOGOZdPdkcuyDhVAyWHGsL4rrV28S+YFZj/VORuoQ8Q=", + "aarch64-linux": "sha256-WRnAaEoKvgFFZ+UkbYtD9gBw0HtV1jdUqv7yUE2uTAQ=", + "aarch64-darwin": "sha256-LxIj/dsL88M99T3WLaD9FL6Qdu2TV+kr1RMZaZ3i4WM=", + "x86_64-darwin": "sha256-PgIvplw6yz9KN5nBWox3BXZIXDbkJ3ZuDPKKSVF82MU=" } } From 7a568a457fd7f1e496e11dc377a0c3edfaa17107 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:39:13 +1000 Subject: [PATCH 245/335] fix: defer MessageV2.Assistant.shape access to break circular dep in compiled binary (#23495) --- packages/opencode/src/session/session.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 5168b80b56..ba144da9f0 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -246,7 +246,8 @@ export const Event = { "session.error", z.object({ sessionID: SessionID.zod.optional(), - error: MessageV2.Assistant.shape.error, + // z.lazy defers access to break circular dep: session → message-v2 → provider → plugin → session + error: z.lazy(() => MessageV2.Assistant.shape.error), }), ), } From 7c6948cf6f90b52a74ad56ac7a7eb16863e65f19 Mon Sep 17 00:00:00 2001 From: opencode Date: Mon, 20 Apr 2026 07:21:46 +0000 Subject: [PATCH 246/335] sync release versions for v1.14.19 --- bun.lock | 32 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop-electron/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++----- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/shared/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 19 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 6dea120fa8..0ba00b23f8 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.14.18", + "version": "1.14.19", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -83,7 +83,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.14.18", + "version": "1.14.19", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -117,7 +117,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.14.18", + "version": "1.14.19", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -144,7 +144,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.14.18", + "version": "1.14.19", "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/openai": "3.0.48", @@ -168,7 +168,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.14.18", + "version": "1.14.19", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -192,7 +192,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.14.18", + "version": "1.14.19", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -225,7 +225,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "1.14.18", + "version": "1.14.19", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -269,7 +269,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.14.18", + "version": "1.14.19", "dependencies": { "@opencode-ai/shared": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -298,7 +298,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.14.18", + "version": "1.14.19", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -314,7 +314,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.14.18", + "version": "1.14.19", "bin": { "opencode": "./bin/opencode", }, @@ -459,7 +459,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.14.18", + "version": "1.14.19", "dependencies": { "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", @@ -494,7 +494,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.14.18", + "version": "1.14.19", "dependencies": { "cross-spawn": "catalog:", }, @@ -509,7 +509,7 @@ }, "packages/shared": { "name": "@opencode-ai/shared", - "version": "1.14.18", + "version": "1.14.19", "bin": { "opencode": "./bin/opencode", }, @@ -533,7 +533,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.14.18", + "version": "1.14.19", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -568,7 +568,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.14.18", + "version": "1.14.19", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -617,7 +617,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.14.18", + "version": "1.14.19", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index a3081798ac..73a648cb6f 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.14.18", + "version": "1.14.19", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 6a837c3731..6f63db1526 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.14.18", + "version": "1.14.19", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 9b92cf0b2b..3605cfb0ee 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.14.18", + "version": "1.14.19", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 6fde7612d4..da73bc61fc 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.14.18", + "version": "1.14.19", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index d45a849368..b66296670f 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.14.18", + "version": "1.14.19", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index 01c6e84f33..7105cb50ef 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "1.14.18", + "version": "1.14.19", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index d3642523ad..7b60658f43 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.14.18", + "version": "1.14.19", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 885d52b9b1..4783381f1f 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.14.18", + "version": "1.14.19", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 7ae4694fb6..cf48deae11 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.14.18" +version = "1.14.19" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.19/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.19/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.19/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.19/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.19/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index a9a935639c..f01d607e3e 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.14.18", + "version": "1.14.19", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 4644922fc3..5d8fd4b540 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.18", + "version": "1.14.19", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index c73addc478..110d6a0916 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.14.18", + "version": "1.14.19", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index f39b575c82..6769ba391c 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.14.18", + "version": "1.14.19", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/shared/package.json b/packages/shared/package.json index b7fffcadb9..cb2b04ee50 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.18", + "version": "1.14.19", "name": "@opencode-ai/shared", "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index 39dc9ab3c5..67dd7ef352 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.14.18", + "version": "1.14.19", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 723cda40d8..9dccd909a8 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.14.18", + "version": "1.14.19", "type": "module", "license": "MIT", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index 51be7fe4a6..d29cc6fc50 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.14.18", + "version": "1.14.19", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index dfddfa9d07..bef2049874 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.14.18", + "version": "1.14.19", "publisher": "sst-dev", "repository": { "type": "git", From 91468fe4556eeb891f586126243272cf217915f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=91=E5=A2=A8=E6=B0=B4=E9=B1=BC?= Date: Mon, 20 Apr 2026 15:35:06 +0800 Subject: [PATCH 247/335] fix(ui): use parentID matching instead of positional scan for assistant messages (#23093) --- packages/ui/src/components/session-turn.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 75279a90e6..61123b180e 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -267,14 +267,12 @@ export function SessionTurn( if (!msg) return emptyAssistant const messages = allMessages() ?? emptyMessages - const index = messageIndex() - if (index < 0) return emptyAssistant + if (messageIndex() < 0) return emptyAssistant const result: AssistantMessage[] = [] - for (let i = index + 1; i < messages.length; i++) { + for (let i = 0; i < messages.length; i++) { const item = messages[i] if (!item) continue - if (item.role === "user") break if (item.role === "assistant" && item.parentID === msg.id) result.push(item as AssistantMessage) } return result From 16caaa222955ae10406d054f2fa84cd78985c09f Mon Sep 17 00:00:00 2001 From: Chris Yang <18487241+ysm-dev@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:32:54 +0900 Subject: [PATCH 248/335] fix(app): fall back to icon.url in sidebar avatar (#18747) --- packages/app/src/pages/layout/sidebar-items.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index b0f45859a4..45db5b5489 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -43,7 +43,9 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti Date: Mon, 20 Apr 2026 09:56:26 -0400 Subject: [PATCH 249/335] zen: tpm based routing --- .../console/app/src/routes/zen/util/modelTpmLimiter.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/console/app/src/routes/zen/util/modelTpmLimiter.ts b/packages/console/app/src/routes/zen/util/modelTpmLimiter.ts index 9a834a1a5b..1fdf360ed0 100644 --- a/packages/console/app/src/routes/zen/util/modelTpmLimiter.ts +++ b/packages/console/app/src/routes/zen/util/modelTpmLimiter.ts @@ -32,13 +32,7 @@ export function createModelTpmLimiter(providers: { id: string; model: string; tp track: async (id: string, model: string, usageInfo: UsageInfo) => { const key = `${id}/${model}` if (!keys.includes(key)) return - const usage = - usageInfo.inputTokens + - usageInfo.outputTokens + - (usageInfo.reasoningTokens ?? 0) + - (usageInfo.cacheReadTokens ?? 0) + - (usageInfo.cacheWrite5mTokens ?? 0) + - (usageInfo.cacheWrite1hTokens ?? 0) + const usage = usageInfo.inputTokens if (usage <= 0) return await Database.use((tx) => tx From d68ebee5557d434ff543d60b1a6c1302ae700682 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 20 Apr 2026 23:58:32 +0800 Subject: [PATCH 250/335] docs(go): add Kimi K2.6 to Go and Zen content (#23558) --- packages/console/app/src/i18n/ar.ts | 8 ++-- packages/console/app/src/i18n/br.ts | 8 ++-- packages/console/app/src/i18n/da.ts | 8 ++-- packages/console/app/src/i18n/de.ts | 8 ++-- packages/console/app/src/i18n/en.ts | 8 ++-- packages/console/app/src/i18n/es.ts | 8 ++-- packages/console/app/src/i18n/fr.ts | 8 ++-- packages/console/app/src/i18n/it.ts | 8 ++-- packages/console/app/src/i18n/ja.ts | 8 ++-- packages/console/app/src/i18n/ko.ts | 8 ++-- packages/console/app/src/i18n/no.ts | 8 ++-- packages/console/app/src/i18n/pl.ts | 8 ++-- packages/console/app/src/i18n/ru.ts | 8 ++-- packages/console/app/src/i18n/th.ts | 8 ++-- packages/console/app/src/i18n/tr.ts | 8 ++-- packages/console/app/src/i18n/zh.ts | 8 ++-- packages/console/app/src/i18n/zht.ts | 8 ++-- packages/console/app/src/routes/go/index.tsx | 46 +++++++++---------- .../routes/workspace/[id]/go/lite-section.tsx | 1 + packages/web/src/content/docs/ar/go.mdx | 7 ++- packages/web/src/content/docs/ar/zen.mdx | 2 + packages/web/src/content/docs/bs/go.mdx | 9 ++-- packages/web/src/content/docs/bs/zen.mdx | 2 + packages/web/src/content/docs/da/go.mdx | 9 ++-- packages/web/src/content/docs/da/zen.mdx | 2 + packages/web/src/content/docs/de/go.mdx | 7 ++- packages/web/src/content/docs/de/zen.mdx | 2 + packages/web/src/content/docs/es/go.mdx | 9 ++-- packages/web/src/content/docs/es/zen.mdx | 2 + packages/web/src/content/docs/fr/go.mdx | 7 ++- packages/web/src/content/docs/fr/zen.mdx | 2 + packages/web/src/content/docs/go.mdx | 9 ++-- packages/web/src/content/docs/it/go.mdx | 9 ++-- packages/web/src/content/docs/it/zen.mdx | 2 + packages/web/src/content/docs/ja/go.mdx | 7 ++- packages/web/src/content/docs/ja/zen.mdx | 2 + packages/web/src/content/docs/ko/go.mdx | 7 ++- packages/web/src/content/docs/ko/zen.mdx | 2 + packages/web/src/content/docs/nb/go.mdx | 9 ++-- packages/web/src/content/docs/nb/zen.mdx | 2 + packages/web/src/content/docs/pl/go.mdx | 9 ++-- packages/web/src/content/docs/pl/zen.mdx | 2 + packages/web/src/content/docs/pt-br/go.mdx | 9 ++-- packages/web/src/content/docs/pt-br/zen.mdx | 2 + packages/web/src/content/docs/ru/go.mdx | 9 ++-- packages/web/src/content/docs/ru/zen.mdx | 2 + packages/web/src/content/docs/th/go.mdx | 7 ++- packages/web/src/content/docs/th/zen.mdx | 2 + packages/web/src/content/docs/tr/go.mdx | 7 ++- packages/web/src/content/docs/tr/zen.mdx | 2 + packages/web/src/content/docs/zen.mdx | 2 + packages/web/src/content/docs/zh-cn/go.mdx | 7 ++- packages/web/src/content/docs/zh-cn/zen.mdx | 2 + packages/web/src/content/docs/zh-tw/go.mdx | 7 ++- packages/web/src/content/docs/zh-tw/zen.mdx | 2 + 55 files changed, 227 insertions(+), 136 deletions(-) diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index f0fdf21804..2df31a213e 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -249,7 +249,7 @@ export const dict = { "go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع", "go.meta.description": - "يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.1 وGLM-5 و Kimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni و Qwen3.5 Plus و Qwen3.6 Plus و MiniMax M2.5 وMiniMax M2.7.", + "يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7.", "go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع", "go.hero.body": "يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.", @@ -298,7 +298,7 @@ export const dict = { "go.problem.item2": "حدود سخية ووصول موثوق", "go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين", "go.problem.item4": - "يتضمن GLM-5.1 وGLM-5 وKimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7", + "يتضمن GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7", "go.how.title": "كيف يعمل Go", "go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.", "go.how.step1.title": "أنشئ حسابًا", @@ -322,7 +322,7 @@ export const dict = { "go.faq.a2": "يتضمن Go النماذج المدرجة أدناه، مع حدود سخية وإتاحة موثوقة.", "go.faq.q3": "هل Go هو نفسه Zen؟", "go.faq.a3": - "لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5.1 وGLM-5 و Kimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni و Qwen3.5 Plus و Qwen3.6 Plus و MiniMax M2.5 وMiniMax M2.7.", + "لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7.", "go.faq.q4": "كم تكلفة Go؟", "go.faq.a4.p1.beforePricing": "تكلفة Go", "go.faq.a4.p1.pricingLink": "$5 للشهر الأول", @@ -345,7 +345,7 @@ export const dict = { "go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟", "go.faq.a9": - "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5.1 وGLM-5 وKimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", + "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", "zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.", "zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index fa479288b6..2546443e94 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -253,7 +253,7 @@ export const dict = { "go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos", "go.meta.description": - "O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.", + "O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.", "go.hero.title": "Modelos de codificação de baixo custo para todos", "go.hero.body": "O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.", @@ -303,7 +303,7 @@ export const dict = { "go.problem.item2": "Limites generosos e acesso confiável", "go.problem.item3": "Feito para o maior número possível de programadores", "go.problem.item4": - "Inclui GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7", + "Inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7", "go.how.title": "Como o Go funciona", "go.how.body": "O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.", @@ -329,7 +329,7 @@ export const dict = { "go.faq.a2": "O Go inclui os modelos listados abaixo, com limites generosos e acesso confiável.", "go.faq.q3": "O Go é o mesmo que o Zen?", "go.faq.a3": - "Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.", + "Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.", "go.faq.q4": "Quanto custa o Go?", "go.faq.a4.p1.beforePricing": "O Go custa", "go.faq.a4.p1.pricingLink": "$5 no primeiro mês", @@ -353,7 +353,7 @@ export const dict = { "go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?", "go.faq.a9": - "Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7 com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).", + "Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7 com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).", "zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.", "zen.api.error.modelNotSupported": "Modelo {{model}} não suportado", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index 9814ece9b5..6cd974c18f 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -251,7 +251,7 @@ export const dict = { "go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle", "go.meta.description": - "Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.", + "Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.", "go.hero.title": "Kodningsmodeller til lav pris for alle", "go.hero.body": "Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.", @@ -300,7 +300,7 @@ export const dict = { "go.problem.item2": "Generøse grænser og pålidelig adgang", "go.problem.item3": "Bygget til så mange programmører som muligt", "go.problem.item4": - "Inkluderer GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7", + "Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7", "go.how.title": "Hvordan Go virker", "go.how.body": "Go starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.", @@ -326,7 +326,7 @@ export const dict = { "go.faq.a2": "Go inkluderer modellerne nedenfor med generøse grænser og pålidelig adgang.", "go.faq.q3": "Er Go det samme som Zen?", "go.faq.a3": - "Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.", + "Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.", "go.faq.q4": "Hvad koster Go?", "go.faq.a4.p1.beforePricing": "Go koster", "go.faq.a4.p1.pricingLink": "$5 første måned", @@ -349,7 +349,7 @@ export const dict = { "go.faq.q9": "Hvad er forskellen på gratis modeller og Go?", "go.faq.a9": - "Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7 med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).", + "Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7 med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).", "zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.", "zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index aa73614932..eaf069f5a6 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -253,7 +253,7 @@ export const dict = { "go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle", "go.meta.description": - "Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7.", + "Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7.", "go.hero.title": "Kostengünstige Coding-Modelle für alle", "go.hero.body": "Go bringt Agentic Coding zu Programmierern auf der ganzen Welt. Mit großzügigen Limits und zuverlässigem Zugang zu den leistungsfähigsten Open-Source-Modellen, damit du mit leistungsstarken Agenten entwickeln kannst, ohne dir Gedanken über Kosten oder Verfügbarkeit zu machen.", @@ -302,7 +302,7 @@ export const dict = { "go.problem.item2": "Großzügige Limits und zuverlässiger Zugang", "go.problem.item3": "Für so viele Programmierer wie möglich gebaut", "go.problem.item4": - "Beinhaltet GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7", + "Beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7", "go.how.title": "Wie Go funktioniert", "go.how.body": "Go beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.", @@ -328,7 +328,7 @@ export const dict = { "go.faq.a2": "Go umfasst die unten aufgeführten Modelle mit großzügigen Limits und zuverlässigem Zugriff.", "go.faq.q3": "Ist Go dasselbe wie Zen?", "go.faq.a3": - "Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7.", + "Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7.", "go.faq.q4": "Wie viel kostet Go?", "go.faq.a4.p1.beforePricing": "Go kostet", "go.faq.a4.p1.pricingLink": "$5 im ersten Monat", @@ -352,7 +352,7 @@ export const dict = { "go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?", "go.faq.a9": - "Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7 mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).", + "Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7 mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).", "zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.", "zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index 86119a560c..5cfd46123b 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -248,7 +248,7 @@ export const dict = { "go.title": "OpenCode Go | Low cost coding models for everyone", "go.meta.description": - "Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7.", + "Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7.", "go.hero.title": "Low cost coding models for everyone", "go.hero.body": "Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.", @@ -296,7 +296,7 @@ export const dict = { "go.problem.item2": "Generous limits and reliable access", "go.problem.item3": "Built for as many programmers as possible", "go.problem.item4": - "Includes GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7", + "Includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7", "go.how.title": "How Go works", "go.how.body": "Go starts at $5 for your first month, then $10/month. You can use it with OpenCode or any agent.", "go.how.step1.title": "Create an account", @@ -321,7 +321,7 @@ export const dict = { "go.faq.a2": "Go includes the models listed below, with generous limits and reliable access.", "go.faq.q3": "Is Go the same as Zen?", "go.faq.a3": - "No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7.", + "No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7.", "go.faq.q4": "How much does Go cost?", "go.faq.a4.p1.beforePricing": "Go costs", "go.faq.a4.p1.pricingLink": "$5 first month", @@ -345,7 +345,7 @@ export const dict = { "go.faq.q9": "What is the difference between free models and Go?", "go.faq.a9": - "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7 with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).", + "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7 with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).", "zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.", "zen.api.error.modelNotSupported": "Model {{model}} not supported", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index bde2bc988e..8127883861 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -254,7 +254,7 @@ export const dict = { "go.title": "OpenCode Go | Modelos de programación de bajo coste para todos", "go.meta.description": - "Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7.", + "Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7.", "go.hero.title": "Modelos de programación de bajo coste para todos", "go.hero.body": "Go lleva la programación agéntica a programadores de todo el mundo. Ofrece límites generosos y acceso fiable a los modelos de código abierto más capaces, para que puedas crear con agentes potentes sin preocuparte por el coste o la disponibilidad.", @@ -304,7 +304,7 @@ export const dict = { "go.problem.item2": "Límites generosos y acceso fiable", "go.problem.item3": "Creado para tantos programadores como sea posible", "go.problem.item4": - "Incluye GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7", + "Incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7", "go.how.title": "Cómo funciona Go", "go.how.body": "Go comienza en $5 el primer mes, luego 10 $/mes. Puedes usarlo con OpenCode o cualquier agente.", "go.how.step1.title": "Crear una cuenta", @@ -329,7 +329,7 @@ export const dict = { "go.faq.a2": "Go incluye los modelos que se indican abajo, con límites generosos y acceso confiable.", "go.faq.q3": "¿Es Go lo mismo que Zen?", "go.faq.a3": - "No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7.", + "No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7.", "go.faq.q4": "¿Cuánto cuesta Go?", "go.faq.a4.p1.beforePricing": "Go cuesta", "go.faq.a4.p1.pricingLink": "$5 el primer mes", @@ -353,7 +353,7 @@ export const dict = { "go.faq.q9": "¿Cuál es la diferencia entre los modelos gratuitos y Go?", "go.faq.a9": - "Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7 con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).", + "Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7 con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).", "zen.api.error.rateLimitExceeded": "Límite de tasa excedido. Por favor, inténtalo de nuevo más tarde.", "zen.api.error.modelNotSupported": "Modelo {{model}} no soportado", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index 867390027f..53f167d7b8 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -255,7 +255,7 @@ export const dict = { "go.title": "OpenCode Go | Modèles de code à faible coût pour tous", "go.meta.description": - "Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7.", + "Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7.", "go.hero.title": "Modèles de code à faible coût pour tous", "go.hero.body": "Go apporte le codage agentique aux programmeurs du monde entier. Offrant des limites généreuses et un accès fiable aux modèles open source les plus capables, pour que vous puissiez construire avec des agents puissants sans vous soucier du coût ou de la disponibilité.", @@ -304,7 +304,7 @@ export const dict = { "go.problem.item2": "Limites généreuses et accès fiable", "go.problem.item3": "Conçu pour autant de programmeurs que possible", "go.problem.item4": - "Inclut GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7", + "Inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7", "go.how.title": "Comment fonctionne Go", "go.how.body": "Go commence à $5 pour le premier mois, puis 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.", @@ -330,7 +330,7 @@ export const dict = { "go.faq.a2": "Go inclut les modèles ci-dessous, avec des limites généreuses et un accès fiable.", "go.faq.q3": "Est-ce que Go est la même chose que Zen ?", "go.faq.a3": - "Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7.", + "Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7.", "go.faq.q4": "Combien coûte Go ?", "go.faq.a4.p1.beforePricing": "Go coûte", "go.faq.a4.p1.pricingLink": "$5 le premier mois", @@ -353,7 +353,7 @@ export const dict = { "Oui, vous pouvez utiliser Go avec n'importe quel agent. Suivez les instructions de configuration dans votre agent de code préféré.", "go.faq.q9": "Quelle est la différence entre les modèles gratuits et Go ?", "go.faq.a9": - "Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7 avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).", + "Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7 avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).", "zen.api.error.rateLimitExceeded": "Limite de débit dépassée. Veuillez réessayer plus tard.", "zen.api.error.modelNotSupported": "Modèle {{model}} non pris en charge", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index 3ca1935dd5..580dad256d 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -251,7 +251,7 @@ export const dict = { "go.title": "OpenCode Go | Modelli di coding a basso costo per tutti", "go.meta.description": - "Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.", + "Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.", "go.hero.title": "Modelli di coding a basso costo per tutti", "go.hero.body": "Go porta il coding agentico ai programmatori di tutto il mondo. Offrendo limiti generosi e un accesso affidabile ai modelli open source più capaci, in modo da poter costruire con agenti potenti senza preoccuparsi dei costi o della disponibilità.", @@ -300,7 +300,7 @@ export const dict = { "go.problem.item2": "Limiti generosi e accesso affidabile", "go.problem.item3": "Costruito per il maggior numero possibile di programmatori", "go.problem.item4": - "Include GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7", + "Include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7", "go.how.title": "Come funziona Go", "go.how.body": "Go inizia a $5 per il primo mese, poi $10/mese. Puoi usarlo con OpenCode o qualsiasi agente.", "go.how.step1.title": "Crea un account", @@ -325,7 +325,7 @@ export const dict = { "go.faq.a2": "Go include i modelli elencati di seguito, con limiti generosi e accesso affidabile.", "go.faq.q3": "Go è lo stesso di Zen?", "go.faq.a3": - "No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.", + "No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.", "go.faq.q4": "Quanto costa Go?", "go.faq.a4.p1.beforePricing": "Go costa", "go.faq.a4.p1.pricingLink": "$5 il primo mese", @@ -349,7 +349,7 @@ export const dict = { "go.faq.q9": "Qual è la differenza tra i modelli gratuiti e Go?", "go.faq.a9": - "I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7 con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).", + "I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7 con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).", "zen.api.error.rateLimitExceeded": "Limite di richieste superato. Riprova più tardi.", "zen.api.error.modelNotSupported": "Modello {{model}} non supportato", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index 7d13dda95b..b21f6a00ed 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -250,7 +250,7 @@ export const dict = { "go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル", "go.meta.description": - "Goは最初の月$5、その後$10/月で、GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7に対して5時間のゆとりあるリクエスト上限があります。", + "Goは最初の月$5、その後$10/月で、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7に対して5時間のゆとりあるリクエスト上限があります。", "go.hero.title": "すべての人のための低価格なコーディングモデル", "go.hero.body": "Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。", @@ -300,7 +300,7 @@ export const dict = { "go.problem.item2": "十分な制限と安定したアクセス", "go.problem.item3": "できるだけ多くのプログラマーのために構築", "go.problem.item4": - "GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7を含む", + "GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7を含む", "go.how.title": "Goの仕組み", "go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。", "go.how.step1.title": "アカウントを作成", @@ -325,7 +325,7 @@ export const dict = { "go.faq.a2": "Go には、十分な利用上限と安定したアクセスを備えた、以下のモデルが含まれます。", "go.faq.q3": "GoはZenと同じですか?", "go.faq.a3": - "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7のオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。", + "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7のオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。", "go.faq.q4": "Goの料金は?", "go.faq.a4.p1.beforePricing": "Goは", "go.faq.a4.p1.pricingLink": "最初の月$5", @@ -349,7 +349,7 @@ export const dict = { "go.faq.q9": "無料モデルとGoの違いは何ですか?", "go.faq.a9": - "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7が含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。", + "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7が含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。", "zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。", "zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index f9ac2e7f38..ce1f076a47 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -247,7 +247,7 @@ export const dict = { "go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델", "go.meta.description": - "Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7에 대해 넉넉한 5시간 요청 한도를 제공합니다.", + "Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7에 대해 넉넉한 5시간 요청 한도를 제공합니다.", "go.hero.title": "모두를 위한 저비용 코딩 모델", "go.hero.body": "Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.", @@ -297,7 +297,7 @@ export const dict = { "go.problem.item2": "넉넉한 한도와 안정적인 액세스", "go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨", "go.problem.item4": - "GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7 포함", + "GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7 포함", "go.how.title": "Go 작동 방식", "go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.", "go.how.step1.title": "계정 생성", @@ -321,7 +321,7 @@ export const dict = { "go.faq.a2": "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 아래 모델이 포함됩니다.", "go.faq.q3": "Go는 Zen과 같은가요?", "go.faq.a3": - "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", + "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", "go.faq.q4": "Go 비용은 얼마인가요?", "go.faq.a4.p1.beforePricing": "Go 비용은", "go.faq.a4.p1.pricingLink": "첫 달 $5", @@ -344,7 +344,7 @@ export const dict = { "go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?", "go.faq.a9": - "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", + "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", "zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.", "zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index b08386f4fe..a4af7b8b8e 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -251,7 +251,7 @@ export const dict = { "go.title": "OpenCode Go | Rimelige kodemodeller for alle", "go.meta.description": - "Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.", + "Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.", "go.hero.title": "Rimelige kodemodeller for alle", "go.hero.body": "Go bringer agent-koding til programmerere over hele verden. Med rause grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene, kan du bygge med kraftige agenter uten å bekymre deg for kostnader eller tilgjengelighet.", @@ -300,7 +300,7 @@ export const dict = { "go.problem.item2": "Rause grenser og pålitelig tilgang", "go.problem.item3": "Bygget for så mange programmerere som mulig", "go.problem.item4": - "Inkluderer GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7", + "Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7", "go.how.title": "Hvordan Go fungerer", "go.how.body": "Go starter på $5 for den første måneden, deretter $10/måned. Du kan bruke det med OpenCode eller hvilken som helst agent.", @@ -326,7 +326,7 @@ export const dict = { "go.faq.a2": "Go inkluderer modellene nedenfor, med høye grenser og pålitelig tilgang.", "go.faq.q3": "Er Go det samme som Zen?", "go.faq.a3": - "Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.", + "Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.", "go.faq.q4": "Hva koster Go?", "go.faq.a4.p1.beforePricing": "Go koster", "go.faq.a4.p1.pricingLink": "$5 første måned", @@ -350,7 +350,7 @@ export const dict = { "go.faq.q9": "Hva er forskjellen mellom gratis modeller og Go?", "go.faq.a9": - "Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7 med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).", + "Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7 med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).", "zen.api.error.rateLimitExceeded": "Rate limit overskredet. Vennligst prøv igjen senere.", "zen.api.error.modelNotSupported": "Modell {{model}} støttes ikke", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index 27d6a9e068..d8abcf70b2 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -252,7 +252,7 @@ export const dict = { "go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego", "go.meta.description": - "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7.", + "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7.", "go.hero.title": "Niskokosztowe modele do kodowania dla każdego", "go.hero.body": "Go udostępnia programowanie z agentami programistom na całym świecie. Oferuje hojne limity i niezawodny dostęp do najzdolniejszych modeli open source, dzięki czemu możesz budować za pomocą potężnych agentów, nie martwiąc się o koszty czy dostępność.", @@ -301,7 +301,7 @@ export const dict = { "go.problem.item2": "Hojne limity i niezawodny dostęp", "go.problem.item3": "Stworzony dla jak największej liczby programistów", "go.problem.item4": - "Zawiera GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7", + "Zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7", "go.how.title": "Jak działa Go", "go.how.body": "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc. Możesz go używać z OpenCode lub dowolnym agentem.", @@ -327,7 +327,7 @@ export const dict = { "go.faq.a2": "Go obejmuje poniższe modele z wysokimi limitami i niezawodnym dostępem.", "go.faq.q3": "Czy Go to to samo co Zen?", "go.faq.a3": - "Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7.", + "Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7.", "go.faq.q4": "Ile kosztuje Go?", "go.faq.a4.p1.beforePricing": "Go kosztuje", "go.faq.a4.p1.pricingLink": "$5 za pierwszy miesiąc", @@ -351,7 +351,7 @@ export const dict = { "go.faq.q9": "Jaka jest różnica między darmowymi modelami a Go?", "go.faq.a9": - "Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7 z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).", + "Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7 z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).", "zen.api.error.rateLimitExceeded": "Przekroczono limit zapytań. Spróbuj ponownie później.", "zen.api.error.modelNotSupported": "Model {{model}} nie jest obsługiwany", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index b4070a9638..8d4d3d4c20 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -255,7 +255,7 @@ export const dict = { "go.title": "OpenCode Go | Недорогие модели для кодинга для всех", "go.meta.description": - "Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7.", + "Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7.", "go.hero.title": "Недорогие модели для кодинга для всех", "go.hero.body": "Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.", @@ -305,7 +305,7 @@ export const dict = { "go.problem.item2": "Щедрые лимиты и надежный доступ", "go.problem.item3": "Создан для максимального числа программистов", "go.problem.item4": - "Включает GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7", + "Включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7", "go.how.title": "Как работает Go", "go.how.body": "Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.", @@ -331,7 +331,7 @@ export const dict = { "go.faq.a2": "Go включает перечисленные ниже модели с щедрыми лимитами и надежным доступом.", "go.faq.q3": "Go — это то же самое, что и Zen?", "go.faq.a3": - "Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7.", + "Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7.", "go.faq.q4": "Сколько стоит Go?", "go.faq.a4.p1.beforePricing": "Go стоит", "go.faq.a4.p1.pricingLink": "$5 за первый месяц", @@ -355,7 +355,7 @@ export const dict = { "go.faq.q9": "В чем разница между бесплатными моделями и Go?", "go.faq.a9": - "Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7 с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", + "Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7 с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", "zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.", "zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index 9455c983f5..ebec3ab867 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -250,7 +250,7 @@ export const dict = { "go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", "go.meta.description": - "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7", + "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7", "go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", "go.hero.body": "Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน", @@ -298,7 +298,7 @@ export const dict = { "go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้", "go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้", "go.problem.item4": - "รวมถึง GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7", + "รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7", "go.how.title": "Go ทำงานอย่างไร", "go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้", "go.how.step1.title": "สร้างบัญชี", @@ -323,7 +323,7 @@ export const dict = { "go.faq.a2": "Go รวมโมเดลด้านล่างนี้ พร้อมขีดจำกัดที่มากและการเข้าถึงที่เชื่อถือได้", "go.faq.q3": "Go เหมือนกับ Zen หรือไม่?", "go.faq.a3": - "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7 อย่างเชื่อถือได้", + "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7 อย่างเชื่อถือได้", "go.faq.q4": "Go ราคาเท่าไหร่?", "go.faq.a4.p1.beforePricing": "Go ราคา", "go.faq.a4.p1.pricingLink": "$5 เดือนแรก", @@ -346,7 +346,7 @@ export const dict = { "go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?", "go.faq.a9": - "โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7 ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)", + "โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7 ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)", "zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง", "zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index a6459b9508..ccc6f1d327 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -253,7 +253,7 @@ export const dict = { "go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri", "go.meta.description": - "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 için cömert 5 saatlik istek limitleri sunar.", + "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 için cömert 5 saatlik istek limitleri sunar.", "go.hero.title": "Herkes için düşük maliyetli kodlama modelleri", "go.hero.body": "Go, dünya çapındaki programcılara ajan tabanlı kodlama getiriyor. En yetenekli açık kaynaklı modellere cömert limitler ve güvenilir erişim sunarak, maliyet veya erişilebilirlik konusunda endişelenmeden güçlü ajanlarla geliştirme yapmanızı sağlar.", @@ -303,7 +303,7 @@ export const dict = { "go.problem.item2": "Cömert limitler ve güvenilir erişim", "go.problem.item3": "Mümkün olduğunca çok programcı için geliştirildi", "go.problem.item4": - "GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 içerir", + "GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 içerir", "go.how.title": "Go nasıl çalışır?", "go.how.body": "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar. OpenCode veya herhangi bir ajanla kullanabilirsiniz.", @@ -329,7 +329,7 @@ export const dict = { "go.faq.a2": "Go, aşağıda listelenen modelleri cömert limitler ve güvenilir erişimle sunar.", "go.faq.q3": "Go, Zen ile aynı mı?", "go.faq.a3": - "Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.", + "Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.", "go.faq.q4": "Go ne kadar?", "go.faq.a4.p1.beforePricing": "Go'nun maliyeti", "go.faq.a4.p1.pricingLink": "İlk ay $5", @@ -353,7 +353,7 @@ export const dict = { "go.faq.q9": "Ücretsiz modeller ve Go arasındaki fark nedir?", "go.faq.a9": - "Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).", + "Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).", "zen.api.error.rateLimitExceeded": "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin.", "zen.api.error.modelNotSupported": "{{model}} modeli desteklenmiyor", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index 5aa82e6fa3..f54bb68736 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -241,7 +241,7 @@ export const dict = { "go.title": "OpenCode Go | 人人可用的低成本编程模型", "go.meta.description": - "Go 首月 $5,之后 $10/月,提供对 GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 的 5 小时充裕请求额度。", + "Go 首月 $5,之后 $10/月,提供对 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 的 5 小时充裕请求额度。", "go.hero.title": "人人可用的低成本编程模型", "go.hero.body": "Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。", @@ -289,7 +289,7 @@ export const dict = { "go.problem.item2": "充裕的限额和可靠的访问", "go.problem.item3": "为尽可能多的程序员打造", "go.problem.item4": - "包含 GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 和 MiniMax M2.7", + "包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 和 MiniMax M2.7", "go.how.title": "Go 如何工作", "go.how.body": "Go 起价为首月 $5,之后 $10/月。您可以将其与 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "创建账户", @@ -311,7 +311,7 @@ export const dict = { "go.faq.a2": "Go 包含下方列出的模型,提供充足的限额和可靠的访问。", "go.faq.q3": "Go 和 Zen 一样吗?", "go.faq.a3": - "不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 等开源模型。", + "不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 等开源模型。", "go.faq.q4": "Go 多少钱?", "go.faq.a4.p1.beforePricing": "Go 费用为", "go.faq.a4.p1.pricingLink": "首月 $5", @@ -333,7 +333,7 @@ export const dict = { "go.faq.q9": "免费模型和 Go 之间的区别是什么?", "go.faq.a9": - "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 和 MiniMax M2.7,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。", + "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 和 MiniMax M2.7,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。", "zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。", "zen.api.error.modelNotSupported": "不支持模型 {{model}}", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index aaaa31386c..4076f18c0d 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -241,7 +241,7 @@ export const dict = { "go.title": "OpenCode Go | 低成本全民編碼模型", "go.meta.description": - "Go 首月 $5,之後 $10/月,提供對 GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 的 5 小時充裕請求額度。", + "Go 首月 $5,之後 $10/月,提供對 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 的 5 小時充裕請求額度。", "go.hero.title": "低成本全民編碼模型", "go.hero.body": "Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。", @@ -289,7 +289,7 @@ export const dict = { "go.problem.item2": "寬裕的限額與穩定存取", "go.problem.item3": "專為盡可能多的程式設計師打造", "go.problem.item4": - "包含 GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 與 MiniMax M2.7", + "包含 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 與 MiniMax M2.7", "go.how.title": "Go 如何運作", "go.how.body": "Go 起價為首月 $5,之後 $10/月。您可以將其與 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "建立帳號", @@ -311,7 +311,7 @@ export const dict = { "go.faq.a2": "Go 包含下方列出的模型,提供充足的額度與穩定的存取。", "go.faq.q3": "Go 與 Zen 一樣嗎?", "go.faq.a3": - "不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 等開源模型。", + "不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 等開源模型。", "go.faq.q4": "Go 費用是多少?", "go.faq.a4.p1.beforePricing": "Go 費用為", "go.faq.a4.p1.pricingLink": "首月 $5", @@ -333,7 +333,7 @@ export const dict = { "go.faq.q9": "免費模型與 Go 有什麼區別?", "go.faq.a9": - "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 與 MiniMax M2.7,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。", + "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 與 MiniMax M2.7,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。", "zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。", "zen.api.error.modelNotSupported": "不支援模型 {{model}}", diff --git a/packages/console/app/src/routes/go/index.tsx b/packages/console/app/src/routes/go/index.tsx index 82b3caf664..b66419c5a7 100644 --- a/packages/console/app/src/routes/go/index.tsx +++ b/packages/console/app/src/routes/go/index.tsx @@ -26,6 +26,7 @@ const models = [ { name: "GLM-5.1", provider: "DeepInfra, Z.ai" }, { name: "GLM-5", provider: "DeepInfra, Z.ai" }, { name: "Kimi K2.5", provider: "Moonshot AI" }, + { name: "Kimi K2.6", provider: "Moonshot AI" }, { name: "MiMo-V2-Pro", provider: "Xiaomi MiMo" }, { name: "MiMo-V2-Omni", provider: "Xiaomi MiMo" }, { name: "Qwen3.5 Plus", provider: "Alibaba Cloud Model Studio" }, @@ -58,8 +59,9 @@ function LimitsGraph(props: { href: string }) { const free = 200 const graph = [ { id: "glm-5.1", name: "GLM-5.1", req: 880, d: "100ms" }, + { id: "kimi-k2.6", name: "Kimi K2.6", req: 1150, d: "150ms" }, { id: "mimo-v2-pro", name: "MiMo-V2-Pro", req: 1290, d: "150ms" }, - { id: "kimi", name: "Kimi K2.5", req: 1850, d: "240ms" }, + { id: "kimi-k2.5", name: "Kimi K2.5", req: 1850, d: "240ms" }, { id: "qwen3.6-plus", name: "Qwen3.6 Plus", req: 3300, d: "280ms" }, { id: "minimax-m2.7", name: "MiniMax M2.7", req: 3400, d: "300ms" }, { id: "qwen3.5-plus", name: "Qwen3.5 Plus", req: 10200, d: "360ms" }, @@ -437,28 +439,26 @@ export default function Home() {
  • {i18n.t("go.faq.a2")} - {language.locale() === "en" && ( -
    - - - - - - - - - - {(m) => ( - - - - - )} - - -
    {i18n.t("workspace.models.table.model")}{i18n.t("workspace.providers.table.provider")}
    {m.name}{m.provider}
    -
    - )} +
    + + + + + + + + + + {(m) => ( + + + + + )} + + +
    {i18n.t("workspace.models.table.model")}{i18n.t("workspace.providers.table.provider")}
    {m.name}{m.provider}
    +
  • diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx index d0f8121828..c894ace104 100644 --- a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx @@ -286,6 +286,7 @@ export function LiteSection() {

    {i18n.t("workspace.lite.promo.modelsTitle")}