From 4b19ea2a71a33e65cd7f3ed19964b1bed1722483 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:05:22 -0500 Subject: [PATCH 001/129] fix(llm): preserve response message phases (#38452) --- .../llm/src/protocols/openai-responses.ts | 270 +++++++++- packages/llm/src/protocols/utils/lifecycle.ts | 17 +- ...-trips-commentary-into-a-final-answer.json | 52 ++ ...ponses-gpt-5-5-reasoning-continuation.json | 8 +- .../openai-responses-phase.recorded.test.ts | 89 ++++ .../test/provider/openai-responses.test.ts | 467 +++++++++++++++++- .../opencode/test/session/llm-native.test.ts | 2 +- 7 files changed, 880 insertions(+), 25 deletions(-) create mode 100644 packages/llm/test/fixtures/recordings/openai-responses-phase/round-trips-commentary-into-a-final-answer.json create mode 100644 packages/llm/test/provider/openai-responses-phase.recorded.test.ts diff --git a/packages/llm/src/protocols/openai-responses.ts b/packages/llm/src/protocols/openai-responses.ts index 4936d31c92..8e2abc9108 100644 --- a/packages/llm/src/protocols/openai-responses.ts +++ b/packages/llm/src/protocols/openai-responses.ts @@ -46,8 +46,12 @@ type OpenAIResponsesInputContent = Schema.Schema.Type + const OpenAIResponsesReasoningSummaryText = Schema.Struct({ type: Schema.tag("summary_text"), text: Schema.String, @@ -78,7 +82,19 @@ const OpenAIResponsesFunctionCallOutput = Schema.Union([ const OpenAIResponsesInputItem = Schema.Union([ Schema.Struct({ role: Schema.tag("system"), content: Schema.String }), Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }), - Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }), + Schema.Struct({ + role: Schema.tag("assistant"), + content: Schema.String, + phase: optionalNull(OpenAIResponsesMessagePhase), + }), + Schema.Struct({ + type: Schema.tag("message"), + id: Schema.String, + status: Schema.Literals(["in_progress", "completed", "incomplete"]), + role: Schema.tag("assistant"), + content: Schema.Array(OpenAIResponsesOutputText), + phase: optionalNull(OpenAIResponsesMessagePhase), + }), OpenAIResponsesReasoningItem, OpenAIResponsesItemReference, Schema.Struct({ @@ -194,7 +210,9 @@ const OpenAIResponsesStreamItem = Schema.Struct({ server_label: Schema.optional(Schema.String), output: Schema.optional(Schema.Unknown), error: Schema.optional(Schema.Unknown), + content: Schema.optional(Schema.Array(Schema.Unknown)), encrypted_content: optionalNull(Schema.String), + phase: optionalNull(OpenAIResponsesMessagePhase), }) type OpenAIResponsesStreamItem = Schema.Schema.Type @@ -212,7 +230,9 @@ const OpenAIResponsesErrorPayload = Schema.Struct({ const OpenAIResponsesEvent = Schema.Struct({ type: Schema.String, delta: Schema.optional(Schema.String), + text: Schema.optional(Schema.String), item_id: Schema.optional(Schema.String), + content_index: Schema.optional(Schema.Number), summary_index: Schema.optional(Schema.Number), item: Schema.optional(OpenAIResponsesStreamItem), response: Schema.optional( @@ -237,10 +257,18 @@ interface ParserState { readonly tools: ToolStream.State readonly hasFunctionCall: boolean readonly lifecycle: Lifecycle.State + readonly messageItems: Readonly> + readonly messageContentIDs: ReadonlySet + readonly nextMessageContentID: number readonly reasoningItems: Readonly> readonly store: boolean | undefined } +interface MessageStreamItem { + readonly providerMetadata?: ProviderMetadata + readonly content: Readonly> +} + type ReasoningSummaryStatus = "active" | "can-conclude" | "concluded" interface ReasoningStreamItem { @@ -298,6 +326,26 @@ const lowerReasoning = (part: ReasoningPart): OpenAIResponsesReasoningInput | un } } +const messagePhase = (part: TextPart): OpenAIResponsesMessagePhase | null | undefined => { + const phase = part.providerMetadata?.openai?.phase + return phase === "commentary" || phase === "final_answer" || phase === null ? phase : undefined +} + +const messageItemID = (part: TextPart) => { + const itemID = part.providerMetadata?.openai?.itemId + return typeof itemID === "string" && itemID.length > 0 ? itemID : undefined +} + +const messageStatus = (part: TextPart) => { + const status = part.providerMetadata?.openai?.status + return status === "in_progress" || status === "completed" || status === "incomplete" ? status : undefined +} + +const messageAnnotations = (part: TextPart) => { + const annotations = part.providerMetadata?.openai?.annotations + return Array.isArray(annotations) ? annotations : [] +} + const hostedToolItemID = (part: ToolResultPart) => { const openai = part.providerMetadata?.openai return ProviderShared.isRecord(openai) && typeof openai.itemId === "string" && openai.itemId.length > 0 @@ -368,17 +416,49 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ } if (message.role === "assistant") { + const inputStart = input.length const content: TextPart[] = [] + let phase: OpenAIResponsesMessagePhase | null | undefined + let itemID: string | undefined + let status: "in_progress" | "completed" | "incomplete" | undefined const reasoningItems: Record = {} const reasoningReferences = new Set() const hostedToolReferences = new Set() const flushText = () => { if (content.length === 0) return - input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) }) + input.push( + itemID + ? { + type: "message", + id: itemID, + status: status ?? "completed", + role: "assistant", + content: content.map((part) => ({ + type: "output_text", + text: part.text, + annotations: messageAnnotations(part), + })), + ...(phase !== undefined ? { phase } : {}), + } + : { + role: "assistant", + content: ProviderShared.joinText(content), + ...(phase !== undefined ? { phase } : {}), + }, + ) content.splice(0, content.length) + phase = undefined + itemID = undefined + status = undefined } for (const part of message.content) { if (part.type === "text") { + const nextPhase = messagePhase(part) + const nextItemID = messageItemID(part) + if (content.length > 0 && (phase !== nextPhase || itemID !== nextItemID)) flushText() + phase = nextPhase + itemID = nextItemID + status = messageStatus(part) ?? status content.push(part) continue } @@ -429,6 +509,20 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ ]) } flushText() + if (store === false && Object.values(reasoningItems).some((item) => typeof item.encrypted_content !== "string")) + input.splice( + inputStart, + input.length - inputStart, + ...input.slice(inputStart).map((item) => + "type" in item && item.type === "message" + ? { + role: "assistant" as const, + content: ProviderShared.joinText(item.content), + ...(item.phase !== undefined ? { phase: item.phase } : {}), + } + : item, + ), + ) continue } @@ -612,15 +706,134 @@ const NO_EVENTS: StepResult["1"] = [] // the protocol's `terminal` predicate stay in sync. const TERMINAL_TYPES = new Set(["response.completed", "response.incomplete", "response.failed"]) -const onOutputTextDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { - if (!event.delta) return [state, NO_EVENTS] +const messageMetadata = (item: OpenAIResponsesStreamItem, id: string, previous?: ProviderMetadata) => { + const openai = previous?.openai + const phase = item.phase !== undefined ? item.phase : openai?.phase + const status = + item.status === "in_progress" || item.status === "completed" || item.status === "incomplete" + ? item.status + : openai?.status + return openaiMetadata({ + itemId: id, + ...(phase === "commentary" || phase === "final_answer" || phase === null ? { phase } : {}), + ...(status === "in_progress" || status === "completed" || status === "incomplete" ? { status } : {}), + }) +} + +const messageContentMetadata = ( + providerMetadata: ProviderMetadata, + item: OpenAIResponsesStreamItem, + index: number, +): ProviderMetadata => { + const content = item.content?.[index] + if (!ProviderShared.isRecord(content) || content.type !== "output_text" || !Array.isArray(content.annotations)) + return providerMetadata + return openaiMetadata({ ...providerMetadata.openai, annotations: content.annotations }) +} + +const ensureMessageContent = (state: ParserState, event: OpenAIResponsesEvent) => { + const itemID = event.item_id ?? "text-0" + const index = event.content_index ?? 0 + const item = state.messageItems[itemID] ?? { content: {} } + const existing = item.content[index] + if (existing) return { state, itemID, index, item, content: existing } + const findID = (next: number): readonly [string, number] => { + const id = `openai-text-${next}` + return state.messageContentIDs.has(id) ? findID(next + 1) : [id, next + 1] + } + const [id, nextMessageContentID] = + index === 0 && !state.messageContentIDs.has(itemID) + ? ([itemID, state.nextMessageContentID] as const) + : findID(state.nextMessageContentID) + const content = { id, text: "" } + const nextItem = { ...item, content: { ...item.content, [index]: content } } + return { + state: { + ...state, + messageItems: { ...state.messageItems, [itemID]: nextItem }, + messageContentIDs: new Set([...state.messageContentIDs, id]), + nextMessageContentID, + }, + itemID, + index, + item: nextItem, + content, + } +} + +const updateMessageContent = ( + state: ParserState, + itemID: string, + index: number, + content: { readonly id: string; readonly text: string }, +): ParserState => ({ + ...state, + messageItems: { + ...state.messageItems, + [itemID]: { + ...state.messageItems[itemID], + content: { ...state.messageItems[itemID]?.content, [index]: content }, + }, + }, +}) + +const closeOtherMessageContent = (state: ParserState, events: LLMEvent[], item: MessageStreamItem, index: number) => + Object.entries(item.content).reduce( + (lifecycle, entry) => + Number(entry[0]) === index ? lifecycle : Lifecycle.textEnd(lifecycle, events, entry[1].id, item.providerMetadata), + state.lifecycle, + ) + +const appendOutputText = (state: ParserState, event: OpenAIResponsesEvent, text: string): StepResult => { + const ensured = ensureMessageContent(state, event) const events: LLMEvent[] = [] + const lifecycle = Lifecycle.textStart( + closeOtherMessageContent(ensured.state, events, ensured.item, ensured.index), + events, + ensured.content.id, + ensured.item.providerMetadata, + ) return [ - { ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, event.item_id ?? "text-0", event.delta) }, + { + ...updateMessageContent(ensured.state, ensured.itemID, ensured.index, { + ...ensured.content, + text: ensured.content.text + text, + }), + lifecycle: Lifecycle.textDelta(lifecycle, events, ensured.content.id, text), + }, events, ] } +const onOutputTextDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { + if (!event.delta) return [state, NO_EVENTS] + return appendOutputText(state, event, event.delta) +} + +const onOutputTextDone = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { + if (event.text === undefined) return [state, NO_EVENTS] + const ensured = ensureMessageContent(state, event) + if (event.text === ensured.content.text) { + if (ensured.state.lifecycle.text.has(ensured.content.id)) return [ensured.state, NO_EVENTS] + const events: LLMEvent[] = [] + return [ + { + ...ensured.state, + lifecycle: Lifecycle.textStart( + closeOtherMessageContent(ensured.state, events, ensured.item, ensured.index), + events, + ensured.content.id, + ensured.item.providerMetadata, + ), + }, + events, + ] + } + if (event.text.startsWith(ensured.content.text)) + return appendOutputText(ensured.state, event, event.text.slice(ensured.content.text.length)) + return [ensured.state, NO_EVENTS] +} + const onReasoningDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { if (!event.delta) return [state, NO_EVENTS] const events: LLMEvent[] = [] @@ -655,6 +868,23 @@ const reasoningMetadata = (item: OpenAIResponsesStreamItem & { id: string }) => // best-effort, not guaranteed. const onOutputItemAdded = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { const item = event.item + if (item?.type === "message" && item.id) { + const existing = state.messageItems[item.id] + return [ + { + ...state, + messageItems: { + ...state.messageItems, + [item.id]: { + ...existing, + providerMetadata: messageMetadata(item, item.id, existing?.providerMetadata), + content: existing?.content ?? {}, + }, + }, + }, + NO_EVENTS, + ] + } if (item && isReasoningItem(item)) { const events: LLMEvent[] = [] return [ @@ -812,6 +1042,32 @@ const onOutputItemDone = Effect.fn("OpenAIResponses.onOutputItemDone")(function* const item = event.item if (!item) return [state, NO_EVENTS] satisfies StepResult + if (item.type === "message" && item.id) { + const events: LLMEvent[] = [] + const itemID = item.id + const messageItem = state.messageItems[itemID] + const { [itemID]: _finished, ...messageItems } = state.messageItems + const providerMetadata = messageMetadata(item, itemID, messageItem?.providerMetadata) + const lifecycle = Object.entries(messageItem?.content ?? {}).reduce( + (lifecycle, entry) => + Lifecycle.textEnd( + lifecycle, + events, + entry[1].id, + messageContentMetadata(providerMetadata, item, Number(entry[0])), + ), + state.lifecycle, + ) + return [ + { + ...state, + lifecycle, + messageItems, + }, + events, + ] satisfies StepResult + } + if (item.type === "function_call") { if (!item.id || !item.call_id || !item.name) return [state, NO_EVENTS] satisfies StepResult const tools = state.tools[item.id] @@ -939,6 +1195,7 @@ const step = (state: ParserState, event: OpenAIResponsesEvent) => { if (event.type === "response.reasoning_summary_part.done") return Effect.succeed(onReasoningSummaryPartDone(state, event)) if (event.type === "response.output_item.added") return Effect.succeed(onOutputItemAdded(state, event)) + if (event.type === "response.output_text.done") return Effect.succeed(onOutputTextDone(state, event)) if (event.type === "response.function_call_arguments.delta") return onFunctionCallArgumentsDelta(state, event) if (event.type === "response.output_item.done") return onOutputItemDone(state, event) if (event.type === "response.completed" || event.type === "response.incomplete") @@ -968,6 +1225,9 @@ export const protocol = Protocol.make({ hasFunctionCall: false, tools: ToolStream.empty(), lifecycle: Lifecycle.initial(), + messageItems: {}, + messageContentIDs: new Set(), + nextMessageContentID: 0, reasoningItems: {}, store: OpenAIOptions.store(request), }), diff --git a/packages/llm/src/protocols/utils/lifecycle.ts b/packages/llm/src/protocols/utils/lifecycle.ts index eb6c95dfbd..8add02ce67 100644 --- a/packages/llm/src/protocols/utils/lifecycle.ts +++ b/packages/llm/src/protocols/utils/lifecycle.ts @@ -14,16 +14,19 @@ export const stepStart = (state: State, events: LLMEvent[]): State => { return { ...state, stepStarted: true } } -export const textDelta = (state: State, events: LLMEvent[], id: string, text: string): State => { +export const textStart = (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata): State => { + if (state.text.has(id)) return state const stepped = stepStart(state, events) - if (stepped.text.has(id)) { - events.push(LLMEvent.textDelta({ id, text })) - return stepped - } - events.push(LLMEvent.textStart({ id }), LLMEvent.textDelta({ id, text })) + events.push(LLMEvent.textStart({ id, ...(providerMetadata ? { providerMetadata } : {}) })) return { ...stepped, text: new Set([...stepped.text, id]) } } +export const textDelta = (state: State, events: LLMEvent[], id: string, text: string): State => { + const started = textStart(state, events, id) + events.push(LLMEvent.textDelta({ id, text })) + return started +} + export const reasoningStart = ( state: State, events: LLMEvent[], @@ -65,7 +68,7 @@ export const reasoningEnd = ( export const textEnd = (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata): State => { if (!state.text.has(id)) return state const stepped = stepStart(state, events) - events.push(LLMEvent.textEnd({ id, providerMetadata })) + events.push(LLMEvent.textEnd({ id, ...(providerMetadata ? { providerMetadata } : {}) })) const text = new Set(stepped.text) text.delete(id) return { ...stepped, text } diff --git a/packages/llm/test/fixtures/recordings/openai-responses-phase/round-trips-commentary-into-a-final-answer.json b/packages/llm/test/fixtures/recordings/openai-responses-phase/round-trips-commentary-into-a-final-answer.json new file mode 100644 index 0000000000..bbb47c4cc0 --- /dev/null +++ b/packages/llm/test/fixtures/recordings/openai-responses-phase/round-trips-commentary-into-a-final-answer.json @@ -0,0 +1,52 @@ +{ + "version": 1, + "metadata": { + "name": "openai-responses-phase/round-trips-commentary-into-a-final-answer", + "recordedAt": "2026-07-23T18:55:21.217Z", + "tags": [ + "prefix:openai-responses-phase", + "provider:openai", + "protocol:openai-responses", + "phase", + "tool" + ] + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.openai.com/v1/responses", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"gpt-5.6-sol\",\"input\":[{\"role\":\"system\",\"content\":\"Before calling get_weather, briefly tell the user you are checking. Then call get_weather exactly once. Do not provide the final answer until its result is available.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":100,\"stream\":true}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_09a288a54615e317016a6263962f6081908c42f100432d7432\",\"object\":\"response\",\"created_at\":1784832918,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":100,\"max_tool_calls\":null,\"model\":\"gpt-5.6-sol\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Get current weather for a city.\",\"name\":\"get_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_09a288a54615e317016a6263962f6081908c42f100432d7432\",\"object\":\"response\",\"created_at\":1784832918,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":100,\"max_tool_calls\":null,\"model\":\"gpt-5.6-sol\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Get current weather for a city.\",\"name\":\"get_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"commentary\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"I\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"TrlggQ3HsX40yjX\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"’ll\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"OT4APBDnZPskz\",\"output_index\":0,\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" check\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"y6OCrBSFwy\",\"output_index\":0,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" the\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"VgUHMFuGRsCe\",\"output_index\":0,\"sequence_number\":7}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" current\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"NfRf0E2g\",\"output_index\":0,\"sequence_number\":8}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" weather\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"FDKK7SxE\",\"output_index\":0,\"sequence_number\":9}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" in\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"VpCRtSlIHUKRx\",\"output_index\":0,\"sequence_number\":10}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" Paris\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"7ksv7FLpVR\",\"output_index\":0,\"sequence_number\":11}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"lo7fPMYb2XF2iIz\",\"output_index\":0,\"sequence_number\":12}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":13,\"text\":\"I’ll check the current weather in Paris.\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"I’ll check the current weather in Paris.\"},\"sequence_number\":14}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"I’ll check the current weather in Paris.\"}],\"phase\":\"commentary\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":15}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_sp8Ji4cqPjnpRbTQ8Kss4epd\",\"name\":\"get_weather\"},\"output_index\":1,\"sequence_number\":16}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"obfuscation\":\"j3afFo6Txi12z5\",\"output_index\":1,\"sequence_number\":17}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"city\",\"item_id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"obfuscation\":\"k5BeAAvHaqNo\",\"output_index\":1,\"sequence_number\":18}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"obfuscation\":\"p7WgYInwBJjJs\",\"output_index\":1,\"sequence_number\":19}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"Paris\",\"item_id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"obfuscation\":\"Gm7YVJP56OJ\",\"output_index\":1,\"sequence_number\":20}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\"}\",\"item_id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"obfuscation\":\"DP3YnNaUFF1AFQ\",\"output_index\":1,\"sequence_number\":21}\n\nevent: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\",\"item_id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"output_index\":1,\"sequence_number\":22}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\",\"call_id\":\"call_sp8Ji4cqPjnpRbTQ8Kss4epd\",\"name\":\"get_weather\"},\"output_index\":1,\"sequence_number\":23}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_09a288a54615e317016a6263962f6081908c42f100432d7432\",\"object\":\"response\",\"created_at\":1784832918,\"status\":\"completed\",\"background\":false,\"completed_at\":1784832920,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":100,\"max_tool_calls\":null,\"model\":\"gpt-5.6-sol\",\"moderation\":null,\"output\":[{\"id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"I’ll check the current weather in Paris.\"}],\"phase\":\"commentary\",\"role\":\"assistant\"},{\"id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\",\"call_id\":\"call_sp8Ji4cqPjnpRbTQ8Kss4epd\",\"name\":\"get_weather\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Get current weather for a city.\",\"name\":\"get_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":86,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":34,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":120},\"user\":null,\"metadata\":{}},\"sequence_number\":24}\n\n" + } + }, + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.openai.com/v1/responses", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"gpt-5.6-sol\",\"input\":[{\"role\":\"system\",\"content\":\"Before calling get_weather, briefly tell the user you are checking. Then call get_weather exactly once. After its result, answer exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]},{\"type\":\"message\",\"id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"status\":\"completed\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"I’ll check the current weather in Paris.\",\"annotations\":[]}],\"phase\":\"commentary\"},{\"type\":\"function_call\",\"call_id\":\"call_sp8Ji4cqPjnpRbTQ8Kss4epd\",\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_sp8Ji4cqPjnpRbTQ8Kss4epd\",\"output\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":100,\"stream\":true}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_09a288a54615e317016a626398499481908cda8e1ed0ef45bd\",\"object\":\"response\",\"created_at\":1784832920,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":100,\"max_tool_calls\":null,\"model\":\"gpt-5.6-sol\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Get current weather for a city.\",\"name\":\"get_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_09a288a54615e317016a626398499481908cda8e1ed0ef45bd\",\"object\":\"response\",\"created_at\":1784832920,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":100,\"max_tool_calls\":null,\"model\":\"gpt-5.6-sol\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Get current weather for a city.\",\"name\":\"get_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"Paris\",\"item_id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"logprobs\":[],\"obfuscation\":\"SQK4VKpPg6o\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" is\",\"item_id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"logprobs\":[],\"obfuscation\":\"1kFOJRFyao7zE\",\"output_index\":0,\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" sunny\",\"item_id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"logprobs\":[],\"obfuscation\":\"n4cE1OQJnX\",\"output_index\":0,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".\",\"item_id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"logprobs\":[],\"obfuscation\":\"wd9OLc2GBwngpGN\",\"output_index\":0,\"sequence_number\":7}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":8,\"text\":\"Paris is sunny.\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Paris is sunny.\"},\"sequence_number\":9}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Paris is sunny.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":10}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_09a288a54615e317016a626398499481908cda8e1ed0ef45bd\",\"object\":\"response\",\"created_at\":1784832920,\"status\":\"completed\",\"background\":false,\"completed_at\":1784832921,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":100,\"max_tool_calls\":null,\"model\":\"gpt-5.6-sol\",\"moderation\":null,\"output\":[{\"id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Paris is sunny.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Get current weather for a city.\",\"name\":\"get_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":140,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":8,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":148},\"user\":null,\"metadata\":{}},\"sequence_number\":11}\n\n" + } + } + ] +} diff --git a/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json b/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json index 47670c8127..cb028796f3 100644 --- a/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json +++ b/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json @@ -2,7 +2,7 @@ "version": 1, "metadata": { "name": "openai-responses/openai-responses-gpt-5-5-reasoning-continuation", - "recordedAt": "2026-05-23T23:19:06.776Z", + "recordedAt": "2026-07-23T18:57:01.137Z", "provider": "openai", "route": "openai-responses", "transport": "http", @@ -33,7 +33,7 @@ "headers": { "content-type": "text/event-stream; charset=utf-8" }, - "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e74e148195beb46e1925d20292\",\"object\":\"response\",\"created_at\":1779578343,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e74e148195beb46e1925d20292\",\"object\":\"response\",\"created_at\":1779578343,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"rs_0a0794dab3b8ec7d016a1235e7ce3881958a5eca32a36a14c5\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXnglldg7hhpTBATVqj7sThK5ATieOVR8sZGYPDW2zYopwpKxA3RyRccK_FPjRvvlzrvL-FitOxmdMGBaKa5jncrT9hHo5IMhsFsCEHkQ1x5tlrKPqtfwJ_LFexR0h_IpPogu8wlVAkHRoWQoq61o9vBxjMOEsq6dtXu09959gXnAvJA3jN_mqNkRZ7Yp6LaJJtLDAAtt_dhX8veoEFXZ412lCY4zcaMvC5o0yq6MPvLIN4NhHmfPKkVAy-j8wGlgA42KR4wd5-VeFXUdeSn32dlNLZZxBFa9w6iTgCQ9aF-3C7RB4OXeSY782QUD1dRyFybd7vJtjlptwXBntSHZ9wugoKSDEj0KnvQKG_WiCWuJvkGiOVno4MAs5QnCmKBnpak5OV1wOhPwX2ez6OmAYT4mMKIogdfivVvUxMrmdVJzgE85WoZEAU2ZporxVXkI7_8p0L6dxxwk_IKiKSCz-bZgsCtOP5Jsr5GeI831nVv272kZ3DugV-hcjGHAE5T9KhebzpFjsdxnJcfxuGY8SyRaLlUAHM_37H4veHsOzyhCoaG8mMaT3gIb4tAvM7ezd1xzLsFae89P5xCv_fNeoV7qmf2IWDWUi1vitIib5w9jsclWRqYaLVZR0GK6dYyNJ1DXDOOcWRdH7UJakv1m2koUbcYWBuxao7sc-af_9ySKAloWhb6QjiVElJHYtwraJBtX-CLBVHEYqAXmZgMUWVbz8NNRA6JS1TrOys7_LiQtXXubLWas_66LyaqmB-628LCUitUISYYc2wmq1uUm7gjPA53Wm4F7VU6g-PO7bt1O0Nd-jasisPXINTX3Z4hgC1APPEq29iEHwmEPnicO_Nu6U3JLfq4DD6r1oLK-RnIp3Ratw0P-Gwog86RBLGUWIEIKdFu6m9d1TI8rIBbAVaBA==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"rs_0a0794dab3b8ec7d016a1235e7ce3881958a5eca32a36a14c5\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXoGMCw3WDXpoD9151PEr2Lt8raW7KBKefQhZJGWx5f8jy152bApO6oE-Mr1BhUtfZNq3OPBVfSL4ioQ9bHREfujIBXgk9LUDBAz2Sle7KjOr9HaUV16A4HBiaFIRFjsHPS9G8yEySp1m6F1CD_WR6apyUGgugRh_y39EcOJmxPOzmiac5DVM6fraA1VpcGbqrZ1x2ANHFDOfnYTycPtPNTgzE7LjkYjDDWbT03uN1YxfP4pqjDVRzY14pA8bSZ8ys-pDv5kUFCAsw-OlU4jYKUXp-M8_6KTaRQP71LPwppt__zG_NJPfy-qUil4pOU8_NoxtxerHgLLXbfExZdzfpoGinoEjn7nj7BJDEtl-LNeNEb5c-1ZymNfVMp-Cs3fLEPkAV8rtHFtZ0MhE_07GKbGo7hTrOmkM4DydxmHsdWGNbXAG35cprslEA5P7p3GHFKnRs5hGs2eq-XcZ3yki64ZBOU_Tv6UR7nUH09gF1rdrJo3dpre6M00COwwdZ02zUP5KxCuI8FKu2jsZu9zgMVXDALsdtM5orTCVLXsn4rddWd111zE-vMjNmMMmktW2cHMjH7j1ooA-9P083koNVYiLi4UhMA64gTqgyl8MxkZekl7eFSMa7qk295NaHOKtFxzYYcZ9jdioCwSPSZ0ZZWLoNgrK7SWfRh0uaTHNcMZ3wq8ae6CguktIeVTCPTQAqJLQqd7AU0oOCKCJ7BWnC-L8UC6m7Pm9ZS958uUVeWBhgKHzMAGq9UeQB7IEeAcbMn3EDgOSfd8qCb8iwU9iG9dcu9axQwWU7pd7kd-T-He61W7z5wWgpx1KehWCxrN6kuKSo6p-uUfwVnJukreOn8BJNAzADQgz68bhmN9VGih7YcKVnLgwDwKditrjSd6-tfE0Baarj3jWENvT6ohY17R9FDrKS-2v8IIX6tGjoKJw8SRhaWLNv4vWlmxRgR0gdac3qumd0GKqsWSveNz01naA==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":3}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":4}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"Hello\",\"item_id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"logprobs\":[],\"obfuscation\":\"3nRhhCWA1H8\",\"output_index\":1,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"!\",\"item_id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"logprobs\":[],\"obfuscation\":\"60NqChSEyXHKsoy\",\"output_index\":1,\"sequence_number\":7}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"logprobs\":[],\"output_index\":1,\"sequence_number\":8,\"text\":\"Hello!\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"},\"sequence_number\":9}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":10}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e74e148195beb46e1925d20292\",\"object\":\"response\",\"created_at\":1779578343,\"status\":\"completed\",\"background\":false,\"completed_at\":1779578344,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[{\"id\":\"rs_0a0794dab3b8ec7d016a1235e7ce3881958a5eca32a36a14c5\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXoMO9Ci_Q06HKQ0YDBarUvbp9ulkR9W2RXWPbx7XKokNCrKUZX-pPPGpUg6r-vTXe8iEX-oED6TmjxZV_nyo838x6pmQJlDqz5JECs2axIrUbCjv9xBt3ob8eAyOizhKFjp3dJNu4i01c38MPZ5QYpD24uCKf69jzjUfydKIEjbo0VhP3K6SDG0V9ZUtua-e6WMqzIg-W5Zs3u64DxGw974ntmvNsx8lsuLR-bk9S5ZZ7zPlCG2Emwfph8UE5HJmIfmMxYlrY5qmXSWKDhse9hovQj-TrvbllP-0vLNQWEPLc3aUfVrWWR9i3NZZ-nxJZiIJPCF3xxIIyKaLh9a6Lh9J6Z-brsvVfbVJWXIGZhsu-uKk6Gwoqo56KqHdNaPF7lkPo5GAWfMrweCnJZ4o_j-oWm8BwTkXxrLib4XYKDO2JNqrNdbmy8rZ7UGgW_DVTiNyZi6LoRfSuvK45MWV2uzB_OJ9LBcqgscY4HyPvKrhGG4Peh4iXuBUCyQQ2IudM5GbeeMOAF3dnEzZff68SwE1H56CO6PtKhVQ6cFJMf7LwI5LFFio0qJnEDx-MejvU7PxmYW7R3MEbgjbsuEFU5KnRVYsgug3_Bq1vXdmP2qhebufFZwz26SwaFqyn3xjwCP8-GR7lWCZ2EvUvWtfxJ5_zgkZg06UsF4Eo_CWKFdp0ao43nemNJxOlMzFa6tPuCgplmD0oYoQ316f-bWK02-eJk56S7G4bZSk8cQfExfIZMjW2f-qrxvfxEpFiXsZF80BQwgRUOeKjsqidg2ihdldRkXGn3vX8p15mf1UgstU8y3DNd2_qJe1f_pEl6rWNXoxFdSRCTG7wTAqbCCmuDgCKGhNQY9tfJNsFqgWBIGkqKy88DN_HiWywJjJ-5u9aoe68yDK-E0TMDqs7ZrTely1wvmkl2yF0XQttaB30taxkIcRR-n0PRO-CRNA_9nJkw9ZsBb8oBjyqWH_mwSijT5g==\",\"summary\":[]},{\"id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":31,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":20,\"output_tokens_details\":{\"reasoning_tokens\":12},\"total_tokens\":51},\"user\":null,\"metadata\":{}},\"sequence_number\":11}\n\n" + "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_05f4a9bec03f8148016a6263faf3bc81959b717b99f194a37c\",\"object\":\"response\",\"created_at\":1784833018,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_05f4a9bec03f8148016a6263faf3bc81959b717b99f194a37c\",\"object\":\"response\",\"created_at\":1784833018,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"rs_05f4a9bec03f8148016a6263fb83748195b13a0e6b77c0dd88\",\"type\":\"reasoning\",\"content\":[],\"encrypted_content\":\"gAAAAABqYmP7gx69qnBLNDRIOVvsxN78GS99z9MYWR_FTPUwnVyq8J470WTXQWQEqp9PF8tLyswAhxg2PkxL7ijhN9190IV6QR1waQUzNd6NLYDc5-GG8mJ0LdKYPABZqFYRWV6MEJhDW2CrR5XiGErIzIlMCWEBkE79DDmLOdyOEVMTtetK_CUuxbBGnQ2_8_16FP8AXCqL6xKBCzFxTsQDa9oKTuMoS7jczBlC71fWBiw_cEfILOIUe7_5K4ze7MJG079Ty580gZCAQ0vteMnKpPKSfugMhKlVB_9Wn3wofeL-Xf8s1QojpIAUgHE_fh9fMNGeMkEsUFKhz_vOQrU7FH3NpGTYs0qKQVJaX9FHrR5EooMMimg2TFqc3GZkHJt9VJe1dhfNzcnybIdyz5s0Y6l-1FSD90_8mHR3ZUdfgOBYrsDfVvXpCUdyn4lQGJMkJYgBqAZpXZHvRw16t9_6stCvsScdb5nGrat54qbSYx1mfXD9S2RPw57vzcnClSpFMzen9oMzPWnBxjQujMzjauz_Ps4UJ_H59XUcvuL8fzWv00qhVvbL59NCMpQFdY0b4TtDuIKT0tgEHRKiHMN4zl3zoVJ9bp8qsxVTBBkIF3Q-Wsf4655dkD3yV0H66Yp3XAO5hvRO-0y8FMMpWcc0N5liLOMKiCKj9XMeGi2p5nF66vkPs3fm880W3S2fyOVsxbTWF_TVDJ1SDaHn_xPzzYQCg047sGOQ9S6jvAfblBqIO9iOV3zOF-Gu5Zv9gbJEywTIPSx_u8sKNAqvlxpZ6zJLOyjipByUvFb27MYJLdPw9fIV9w5D2XVwowB8Bg13joWKgm5ne75-VudmIlm-AoWC78zT2_WIKpIN0jYhJDOgxIgpFeI4LEGKh6BlVSQ0-CRrOGw1FnsxKfGN0FL9ChLs-rz4yA==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"rs_05f4a9bec03f8148016a6263fb83748195b13a0e6b77c0dd88\",\"type\":\"reasoning\",\"content\":[],\"encrypted_content\":\"gAAAAABqYmP8aJHmIOttZSqCfZrRqPkxHEGRKL8agRHsomAtpHOM91zNm-YwW-Tupi5CFHy3SFlIatT1_VVZLSR50Mf1jZezXHUkIdB003BGPUwaeHlhlq3B63dukCmAGJso8xJvDw0YqYppy2FXNIoPoys3wI4n8LSTiUOm6yXEyUkaBt4sXLeWsg_NLqYo-ilmv60ppKV1vPQNZkHAX2N0PCZebSmzto5a896xV7jkKVsEMhanPnKP8N3G_ag_-B4ZPr01IAma4xJ2jXysRX_P-zN7AqGDRxBuuoIzylLizo68wNH9cXmnvQcCEkjKxOTpmQGCl0LxSnK_UnaLwErtj9FUMXDaDqbfugCZrCy5pUq0RgBSoAnZvOqH6pWpXPvclQI9Wsg-Ra3bv8sYtf2qYBMebaNIbI_BVVKN8Lrpj7UWODiRZImgfUWHuflaf98H1oD09f7e55gg7GlQ0r6EVZwbuX3GaEwsHbDe7KdsPmwV_RV_ZQpxm14aBypGtapXEAd8SMJxygZtuQKCTJuOwsgZug_I1cGw4EF5AIg5EWzVhvJmlzPrBqas-VdL0_VIfohJAm9181eYKX5ITeUZkdrNpwk7yMCLG4yHCOiP_x33Gyz3xOOUzNSVMy56F7f_K1Bd-eqHK4zMsYrS4-c2cbC5HPPNUUFh8-CaMaJ-34P2TvTHrxR9QY6PXMyfiEMhNC8Mpblhh3ShCvNWTAsrujWHmtDRp47cSNsiNxTSRMU75OsDviGcnuNCZDHBA-92N9g-nPqBvqJfFJXsHbpNzLRCgkv3O8ZOvMPOLLM4OZu1fELgAdKqWNupZgys6n1pUVsRGctECeVZ4NOafuvHvm6wUHRXuLLUN6ATJJ4RdSD_DAtnGD1QFstXwEeBHgBwMMppaIgBvZGbz5skrDm5NBv7AEdCl6UzZ-0zWP6AFxowTzTNuHUyIO-Buwvq_hmLn2hemhr0wOBBgTqFcW51cnybHt7sQD6AhGYlLjNnGu5XH7qcdTY=\",\"summary\":[]},\"output_index\":0,\"sequence_number\":3}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_05f4a9bec03f8148016a6263fc25a08195b4a994aeeed62684\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":4}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_05f4a9bec03f8148016a6263fc25a08195b4a994aeeed62684\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"Hello\",\"item_id\":\"msg_05f4a9bec03f8148016a6263fc25a08195b4a994aeeed62684\",\"logprobs\":[],\"obfuscation\":\"l9gChAJHAvW\",\"output_index\":1,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"!\",\"item_id\":\"msg_05f4a9bec03f8148016a6263fc25a08195b4a994aeeed62684\",\"logprobs\":[],\"obfuscation\":\"MgDcWbMrxjIR4h6\",\"output_index\":1,\"sequence_number\":7}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_05f4a9bec03f8148016a6263fc25a08195b4a994aeeed62684\",\"logprobs\":[],\"output_index\":1,\"sequence_number\":8,\"text\":\"Hello!\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_05f4a9bec03f8148016a6263fc25a08195b4a994aeeed62684\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"},\"sequence_number\":9}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_05f4a9bec03f8148016a6263fc25a08195b4a994aeeed62684\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":10}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_05f4a9bec03f8148016a6263faf3bc81959b717b99f194a37c\",\"object\":\"response\",\"created_at\":1784833018,\"status\":\"completed\",\"background\":false,\"completed_at\":1784833020,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[{\"id\":\"rs_05f4a9bec03f8148016a6263fb83748195b13a0e6b77c0dd88\",\"type\":\"reasoning\",\"content\":[],\"encrypted_content\":\"gAAAAABqYmP8gH5Qc4oBXBDbJyFBgvZVodKHctAOFeZp3khLa37IbJljUkbx7ERw0pgedvhUBaQsRKB31czAty8Ruv9PJVbyoSUpVamjmVM0-FD9lHpwNDnLPjhjPzcAxQosXeVGuWp6jxwmKjzIowwLFp7ImRG2syV-2-QqZ6Wef4VuPzmQ6RVVQUeqBz5Pvhf0FKkqzgb9T3vv0bV-V8qFsdbNKGtQvKKciR5fL6YaUlmY0gsru8UDabWjNjL8oyjZI0HezNcvYNLfmmzptyYpfw208LFiTeD8ntf6xClGgbBztiMQDQhOjQoeaZrrOCaB3iqxbe-_x8xAMRdUpS9NmhJ8oHH1w4JaPQ-Gjvzrrjq0kqpVYXHmdZ-cmwPpRG_0mh4yHkIUYXb0cvoaMyKWLXGcVf3OWLmkEZCa0GuOvwKz8QaPoOAer0L-GrV4p1GadjEUG07EvwsSRyG3fPckMJsSLxW70NNipaGmJys-ihR5YU5qqz74EnpJpYFJnh7Anl5ETl92-ZA4tqAevrW2dwoCNIfGkuX7GMrpPLxTs5sZ1UQWppPyfji-f0YY7198_Ypf8XZfWhB46HYipjSwqErvUyHpE6UpkqWQwwX5wa29clrUrjxTnn1qiw0G2PgvKziwBpImhfWDRGafirQ9G4lh1I8GS5K7vDPVtOPnzxFccYFX4oB5NgSdADMtm5cFKgAqVNM4l04E2FMYJvPr5qn79EWf9AwfOFIuwwNE_sWSkN8VU1-iob0_03iMJ_U2lbT9BT65aJhhSmFUfaqyliC9rUwnFXYEgoFC7jAZ6_LhNJ4ONM26KPUhGu0Uv5xq3aAyk_EKQ_quvYF7euqijnIAzjk8rsV7w7_Qj0Ujnoz7M7QSJx5VaGUCXR8O_pKBcBVLayBe957WZ154qfTYIK96FyOcqM5_RGHlvvfVRi3pnbrQR_odU1e2pAL5hdCyvKtZ3GtN0kaGU1wJOersW5QCUyTIJ9H9ax-YJn3_4Xe4PKKEr5Q=\",\"summary\":[]},{\"id\":\"msg_05f4a9bec03f8148016a6263fc25a08195b4a994aeeed62684\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":31,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":21,\"output_tokens_details\":{\"reasoning_tokens\":13},\"total_tokens\":52},\"user\":null,\"metadata\":{}},\"sequence_number\":11}\n\n" } }, { @@ -44,14 +44,14 @@ "headers": { "content-type": "application/json" }, - "body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Think briefly, then reply exactly with: Hello!\"}]},{\"type\":\"reasoning\",\"summary\":[],\"encrypted_content\":\"gAAAAABqEjXoGMCw3WDXpoD9151PEr2Lt8raW7KBKefQhZJGWx5f8jy152bApO6oE-Mr1BhUtfZNq3OPBVfSL4ioQ9bHREfujIBXgk9LUDBAz2Sle7KjOr9HaUV16A4HBiaFIRFjsHPS9G8yEySp1m6F1CD_WR6apyUGgugRh_y39EcOJmxPOzmiac5DVM6fraA1VpcGbqrZ1x2ANHFDOfnYTycPtPNTgzE7LjkYjDDWbT03uN1YxfP4pqjDVRzY14pA8bSZ8ys-pDv5kUFCAsw-OlU4jYKUXp-M8_6KTaRQP71LPwppt__zG_NJPfy-qUil4pOU8_NoxtxerHgLLXbfExZdzfpoGinoEjn7nj7BJDEtl-LNeNEb5c-1ZymNfVMp-Cs3fLEPkAV8rtHFtZ0MhE_07GKbGo7hTrOmkM4DydxmHsdWGNbXAG35cprslEA5P7p3GHFKnRs5hGs2eq-XcZ3yki64ZBOU_Tv6UR7nUH09gF1rdrJo3dpre6M00COwwdZ02zUP5KxCuI8FKu2jsZu9zgMVXDALsdtM5orTCVLXsn4rddWd111zE-vMjNmMMmktW2cHMjH7j1ooA-9P083koNVYiLi4UhMA64gTqgyl8MxkZekl7eFSMa7qk295NaHOKtFxzYYcZ9jdioCwSPSZ0ZZWLoNgrK7SWfRh0uaTHNcMZ3wq8ae6CguktIeVTCPTQAqJLQqd7AU0oOCKCJ7BWnC-L8UC6m7Pm9ZS958uUVeWBhgKHzMAGq9UeQB7IEeAcbMn3EDgOSfd8qCb8iwU9iG9dcu9axQwWU7pd7kd-T-He61W7z5wWgpx1KehWCxrN6kuKSo6p-uUfwVnJukreOn8BJNAzADQgz68bhmN9VGih7YcKVnLgwDwKditrjSd6-tfE0Baarj3jWENvT6ohY17R9FDrKS-2v8IIX6tGjoKJw8SRhaWLNv4vWlmxRgR0gdac3qumd0GKqsWSveNz01naA==\"},{\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello!\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Now reply exactly with: Done.\"}]}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"low\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":40,\"stream\":true}" + "body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Think briefly, then reply exactly with: Hello!\"}]},{\"type\":\"reasoning\",\"summary\":[],\"encrypted_content\":\"gAAAAABqYmP8aJHmIOttZSqCfZrRqPkxHEGRKL8agRHsomAtpHOM91zNm-YwW-Tupi5CFHy3SFlIatT1_VVZLSR50Mf1jZezXHUkIdB003BGPUwaeHlhlq3B63dukCmAGJso8xJvDw0YqYppy2FXNIoPoys3wI4n8LSTiUOm6yXEyUkaBt4sXLeWsg_NLqYo-ilmv60ppKV1vPQNZkHAX2N0PCZebSmzto5a896xV7jkKVsEMhanPnKP8N3G_ag_-B4ZPr01IAma4xJ2jXysRX_P-zN7AqGDRxBuuoIzylLizo68wNH9cXmnvQcCEkjKxOTpmQGCl0LxSnK_UnaLwErtj9FUMXDaDqbfugCZrCy5pUq0RgBSoAnZvOqH6pWpXPvclQI9Wsg-Ra3bv8sYtf2qYBMebaNIbI_BVVKN8Lrpj7UWODiRZImgfUWHuflaf98H1oD09f7e55gg7GlQ0r6EVZwbuX3GaEwsHbDe7KdsPmwV_RV_ZQpxm14aBypGtapXEAd8SMJxygZtuQKCTJuOwsgZug_I1cGw4EF5AIg5EWzVhvJmlzPrBqas-VdL0_VIfohJAm9181eYKX5ITeUZkdrNpwk7yMCLG4yHCOiP_x33Gyz3xOOUzNSVMy56F7f_K1Bd-eqHK4zMsYrS4-c2cbC5HPPNUUFh8-CaMaJ-34P2TvTHrxR9QY6PXMyfiEMhNC8Mpblhh3ShCvNWTAsrujWHmtDRp47cSNsiNxTSRMU75OsDviGcnuNCZDHBA-92N9g-nPqBvqJfFJXsHbpNzLRCgkv3O8ZOvMPOLLM4OZu1fELgAdKqWNupZgys6n1pUVsRGctECeVZ4NOafuvHvm6wUHRXuLLUN6ATJJ4RdSD_DAtnGD1QFstXwEeBHgBwMMppaIgBvZGbz5skrDm5NBv7AEdCl6UzZ-0zWP6AFxowTzTNuHUyIO-Buwvq_hmLn2hemhr0wOBBgTqFcW51cnybHt7sQD6AhGYlLjNnGu5XH7qcdTY=\"},{\"role\":\"assistant\",\"content\":\"Hello!\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Now reply exactly with: Done.\"}]}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"low\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":40,\"stream\":true}" }, "response": { "status": 200, "headers": { "content-type": "text/event-stream; charset=utf-8" }, - "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e991c88195a4d2f9766babd985\",\"object\":\"response\",\"created_at\":1779578345,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e991c88195a4d2f9766babd985\",\"object\":\"response\",\"created_at\":1779578345,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"rs_0a0794dab3b8ec7d016a1235ea4dd88195a32179255ed6c532\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXqB-kOX_0QAeoEksgNjwSbtGmVEQuMj5ODcFV6b7Kp3E8RoHRRmSXtRH0rtNbZRbhKz5jM48DUpDI1WTeO2HqCd_A3fsSgFxp5ACGVFjPWjfvP2JMdDkpoOo5gu2zy7WsWY0fseocQQ5q_jfG6SWw0fyaeeqfdQ9HkcHyg6gVEl5skb4L8_2lD5nClmLlNVVh5JCuXRH9eYysrfO19NOZ29A2MVUX-XgB6mmK5uSb1jE43GhrEPPYrMbB5JyzM6B-yeB8rE4H2wx530hQqtxwSZREa8G03rzTJ49_KAPWl0djGDDtufUX-t4EpBHo6loA3PMuiZ3VsJTkkPpEqkm6QQyAVkQ_8AdRu12CqHbFdu73I-BnArzr33yW6reNUjnZjFV5bWDyxIMh6ljy3O_2nGk-qdTLt6bGJbEjTdPj1hi7icYZTVPqofPU4pjlo9BnIBheo-4u9pA26V9G9vDAtM4myDdMEe4pnieUztBUYPUOVMaG2U9gqtNs6iPehKo3BeKy4lhYPorL2OPmf1lVUQOCW1MBbwT5xt1kjOVw7LggnyjrBsXVvDBWg0AFcvm14r3ZQezPgLetQfSx56mVEJpui9BuVSUg2Xvqb5tCCip6TipUVvzZJKKkxN43o8N6UVXLIn6wgstAn9727JgBEsjMxzvuOWaaI-qM3dWMcFzFSGvKb6gTiF37AhSOzosf31hnsGx8AnGmLmbuW3IMhZXZZMHgVUHx4p8pNRPeoCE-Bv833KZbRfVxe6tbmkLadBjXYaCQqPXDHBR7qbpfu4_M9UAy0kpic-joepxQKsCT2t6vsNaThRYaN3PtIgjs5xxAfa9yKeIYak8yiP96CUlME9Y7zaOIydPWYBhWLf3phQsWMax9eKdLDh19f0Y0iAJPpk--1xd2OVTuDxKIMpA==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"rs_0a0794dab3b8ec7d016a1235ea4dd88195a32179255ed6c532\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXqW5MInCBPKulZ1lizyFtOaKUpKgHldAXVjTTs4XFE45gtxC1NbJoOi2tHoQhpfq-JGxtjSQEDTHnMCiLLhyqvQ4GlWVF4n51xcFVC_WgymkZqDxG6xPw8ITAsRI5vb8HiPO6EmmKt6xGIVXOjrrxRNAY3xtrByeYSvnCa6FDUHEkMeXmwllBalCeQPNDPl0Ub2ehuchNG0loMVLJoOjT-2KgDrXlOa6rCn3nUf4U1W5JA_kHytlgrD0IPbs7nY8wemdynJRXBoNSOT_U3nQSB6j-i4KIJAdLiUs9LVWMYleqmFQNs8S4dC3i5DfpHXWUMZ5Ai1d3gbvMP8bH7fsUyfIhyiDUvlgr6PZ9rfh8JqkjOpiQ7NFtSDuHQGdx__W3qi23WPDp3iQjKxVl1oUXfbMzsPE4bmNN9dnJ9qTQ43kvw8GyrGrSqRS8jCKuk9bxqeR_ibj4KoDdxvVbUeGMg3WKANfCRsNXlxwYtMpu3I4HxKm5EuMNKDg_e9RFH2wFEDm9wCKMZrC_5LShgKhSfhsk3yJ47Mit0zYdX27kyHGlZvpzLPYKdtHW1O15KNT4gFKBrIguCtjXz2Lb42ENM6Jo8BTY7BZbf0hXZ4A5mFNl_gyLVHWEHpR1GcYiJbs9RtQ-7qX_PTeZ11iFY3a7_jM7TK3WEN2IuG0OKbZVHvOkVcvyBEgIbzSzCzhtC-j584knI4WmYiqnltuwRcR2N3sxYY3vMcYGA2_AU5kYlZcJcztapTTW-aKbyGxPcw5D_dqb5mGpDqJgquye-qOufDt4Fd7cSc-g8awqR8QPsLz-ZDPLMB9JKQ3VqLQlNCKDUoDodGOAL3h-7EQG66osALfhpdsWcNmuVqlb0lNAXklrsZJtRKBU4pJ1UGCyVDwde7nv6I9PW19VumaRrJhc2cC52qUoyihvUo8xJsElaFp7-EHn5ymS4znZhRfyA_4UDL4rwj-3DqbMwNeDJrgBc3w==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":3}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":4}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"Done\",\"item_id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"logprobs\":[],\"obfuscation\":\"7tyE7hMvNOTM\",\"output_index\":1,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".\",\"item_id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"logprobs\":[],\"obfuscation\":\"RGXvuTTSJS3AT5E\",\"output_index\":1,\"sequence_number\":7}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"logprobs\":[],\"output_index\":1,\"sequence_number\":8,\"text\":\"Done.\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Done.\"},\"sequence_number\":9}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Done.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":10}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e991c88195a4d2f9766babd985\",\"object\":\"response\",\"created_at\":1779578345,\"status\":\"completed\",\"background\":false,\"completed_at\":1779578346,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[{\"id\":\"rs_0a0794dab3b8ec7d016a1235ea4dd88195a32179255ed6c532\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXq8oliF2VeqiOUi-jUdi49emjffD6wtbmxlwQWbJ6tSxXIjyXvCeclOqKx83G83GyDOJqvR4L_D8V_ebJtgG87ahWB8Rr9LEQDoLT24n4Vz279xtHMxEGgv7f0NmaXu2dGFeFY_s2RhH-DqNE7V4nEkS7odJOTkhxTKgEcxtz3dDlEnGU7IgN2sD1lh9y90BD3ysvARegy4Cs0DhUjLOvkx11G9lk5dQ3yo1ek8JhTHpnVSYrLDYIudCh6pfu1yP1tx8xbxDHUcwlNclU9Hp_9ils5FhZNWC_tiLDscXXvRPBgMF77jdOicCV6cyUV0Snsu1_KSRbm4rLtgXLXVMqFyYpxdyicsD577e4yZ0VVXT4Oo_af0eDh3I3ZPIWui38EmYuoRhvQuYZkqjhGd_xOkvjQF4_Tp6cyNO0XdAMGMoYG-5npHC0gcPpv56qYGX8ffj0P8ZyR9shn3H7kcQqE2YXXBa42VKK0poPbC996xSqFNW7ygePel41h493XlJ70wnP50vFY5s0raNFf9eLP3YYmLxiPks9gshayGwUQXNNwrSimoQv3OeJzRzihbzZNWTfhR4xKs53nlXMjwnnXwHRH5D07vJg_1zU7BQzJ-QRLZnsnhIOq3psHt1yuoCtsSTKBN6HPiR81F-snIttJiUAiYsgv_ajwPxxnKP0FnFXQfBuaUAtAOD5G_3MC1yECjzq-YI4MDOXj4dsIGnHkdzXo-DV2lXMl2WnPqytoUkugp14SWbJso-eDsN5QivqspnYc1VsdNAaOOgjBiHmi-bACI1CykrkuiYJm1nOHAH4L4IQjpd0pcNm-Dk7z9LGIE5lwKI07hLXp_ByhVXRT8xWuugl43pzoM1jgYD4LjTjScC3ymauqqvKjjoHfnt0Zma0eVDeQrnVT6W9RQ9wDt5KVebrrwJTqlaNV0HywZJo3gwFy-Qq5MfwAwwC-GdjMsER1TgXO_E5kFZZD4sNVgw==\",\"summary\":[]},{\"id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Done.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":35,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":20,\"output_tokens_details\":{\"reasoning_tokens\":12},\"total_tokens\":55},\"user\":null,\"metadata\":{}},\"sequence_number\":11}\n\n" + "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_06418d4b810c5ed5016a6263fc610081909e978e5ca1a9edd0\",\"object\":\"response\",\"created_at\":1784833020,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_06418d4b810c5ed5016a6263fc610081909e978e5ca1a9edd0\",\"object\":\"response\",\"created_at\":1784833020,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_06418d4b810c5ed5016a6263fd13888190829edf0774b5b6e1\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_06418d4b810c5ed5016a6263fd13888190829edf0774b5b6e1\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"Done\",\"item_id\":\"msg_06418d4b810c5ed5016a6263fd13888190829edf0774b5b6e1\",\"logprobs\":[],\"obfuscation\":\"M3ODGG2egQCH\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".\",\"item_id\":\"msg_06418d4b810c5ed5016a6263fd13888190829edf0774b5b6e1\",\"logprobs\":[],\"obfuscation\":\"fm15a3vpfYAW4br\",\"output_index\":0,\"sequence_number\":5}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_06418d4b810c5ed5016a6263fd13888190829edf0774b5b6e1\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":6,\"text\":\"Done.\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_06418d4b810c5ed5016a6263fd13888190829edf0774b5b6e1\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Done.\"},\"sequence_number\":7}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_06418d4b810c5ed5016a6263fd13888190829edf0774b5b6e1\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Done.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":8}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_06418d4b810c5ed5016a6263fc610081909e978e5ca1a9edd0\",\"object\":\"response\",\"created_at\":1784833020,\"status\":\"completed\",\"background\":false,\"completed_at\":1784833021,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[{\"id\":\"msg_06418d4b810c5ed5016a6263fd13888190829edf0774b5b6e1\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Done.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":35,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":6,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":41},\"user\":null,\"metadata\":{}},\"sequence_number\":9}\n\n" } } ] diff --git a/packages/llm/test/provider/openai-responses-phase.recorded.test.ts b/packages/llm/test/provider/openai-responses-phase.recorded.test.ts new file mode 100644 index 0000000000..f8ea260541 --- /dev/null +++ b/packages/llm/test/provider/openai-responses-phase.recorded.test.ts @@ -0,0 +1,89 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" +import { LLM, Message } from "../../src" +import * as OpenAI from "../../src/providers/openai" +import { OpenAIResponses } from "../../src/protocols/openai-responses" +import { LLMClient } from "../../src/route" +import { weatherTool } from "../recorded-scenarios" +import { recordedTests } from "../recorded-test" + +const model = OpenAI.configure({ + apiKey: process.env.OPENAI_API_KEY ?? "fixture", +}).responses("gpt-5.6-sol") + +const recorded = recordedTests({ + prefix: "openai-responses-phase", + provider: "openai", + protocol: "openai-responses", + requires: ["OPENAI_API_KEY"], +}) + +describe("OpenAI Responses phase recorded", () => { + recorded.effect.with("round-trips commentary into a final answer", { tags: ["phase", "tool"] }, () => + Effect.gen(function* () { + const user = Message.user("What is the weather in Paris?") + const first = yield* LLMClient.generate( + LLM.request({ + model, + system: + "Before calling get_weather, briefly tell the user you are checking. Then call get_weather exactly once. Do not provide the final answer until its result is available.", + messages: [user], + tools: [weatherTool], + generation: { maxTokens: 100 }, + }), + ) + const call = first.toolCalls[0] + if (!call) throw new Error("OpenAI Responses did not return the expected weather tool call") + + expect(call).toMatchObject({ name: "get_weather", input: { city: "Paris" } }) + const commentary = first.message.content.find( + (part) => part.type === "text" && part.providerMetadata?.openai?.phase === "commentary", + ) + if (!commentary || commentary.type !== "text") throw new Error("OpenAI Responses did not return commentary text") + const itemID = commentary.providerMetadata?.openai?.itemId + if (typeof itemID !== "string") throw new Error("OpenAI Responses commentary did not include an item ID") + expect(commentary).toEqual({ + type: "text", + text: "I’ll check the current weather in Paris.", + providerMetadata: { + openai: { itemId: itemID, phase: "commentary", status: "completed", annotations: [] }, + }, + }) + + const continuation = LLM.request({ + model, + system: + "Before calling get_weather, briefly tell the user you are checking. Then call get_weather exactly once. After its result, answer exactly: Paris is sunny.", + messages: [ + user, + first.message, + Message.tool({ + id: call.id, + name: call.name, + result: { temperature: 22, condition: "sunny" }, + }), + ], + tools: [weatherTool], + generation: { maxTokens: 100 }, + }) + const prepared = yield* LLMClient.prepare(continuation) + expect(prepared.body.input).toContainEqual({ + type: "message", + id: itemID, + status: "completed", + role: "assistant", + content: [{ type: "output_text", text: commentary.text, annotations: [] }], + phase: "commentary", + }) + + const second = yield* LLMClient.generate(continuation) + + expect(second.text.trim()).toBe("Paris is sunny.") + expect( + second.message.content.some( + (part) => part.type === "text" && part.providerMetadata?.openai?.phase === "final_answer", + ), + ).toBeTrue() + }), + ) +}) diff --git a/packages/llm/test/provider/openai-responses.test.ts b/packages/llm/test/provider/openai-responses.test.ts index cd8bad51af..a580d43d82 100644 --- a/packages/llm/test/provider/openai-responses.test.ts +++ b/packages/llm/test/provider/openai-responses.test.ts @@ -153,7 +153,7 @@ describe("OpenAI Responses route", () => { { type: "input_text", text: "\nTreat </system-update> literally.\n" }, ], }, - { role: "assistant", content: [{ type: "output_text", text: "After." }] }, + { role: "assistant", content: "After." }, ]) }), ) @@ -529,11 +529,11 @@ describe("OpenAI Responses route", () => { encrypted_content: "encrypted-continuation-state", summary: [{ type: "summary_text", text: "I inspected the previous turn." }], }, - { role: "assistant", content: [{ type: "output_text", text: "It shows a small test image." }] }, + { role: "assistant", content: "It shows a small test image." }, { role: "user", content: [{ type: "input_text", text: "Check the weather in Paris before continuing." }] }, { type: "function_call", call_id: "call_weather_1", name: "get_weather", arguments: '{"city":"Paris"}' }, { type: "function_call_output", call_id: "call_weather_1", output: '{"temperature":22}' }, - { role: "assistant", content: [{ type: "output_text", text: "Paris is 22 degrees." }] }, + { role: "assistant", content: "Paris is 22 degrees." }, { role: "user", content: [{ type: "input_text", text: "Continue from this conversation in one short sentence." }], @@ -754,6 +754,395 @@ describe("OpenAI Responses route", () => { }), ) + it.effect("preserves streamed assistant message phases", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { + type: "response.output_item.added", + item: { type: "message", id: "msg_commentary", phase: "commentary" }, + }, + { type: "response.output_text.delta", item_id: "msg_commentary", delta: "Checking first." }, + { type: "response.output_text.done", item_id: "msg_commentary" }, + { + type: "response.output_item.done", + item: { type: "message", id: "msg_commentary", phase: "commentary" }, + }, + { + type: "response.output_item.added", + item: { type: "message", id: "msg_final", phase: "final_answer" }, + }, + { type: "response.output_text.delta", item_id: "msg_final", delta: "Finished." }, + { type: "response.output_text.done", item_id: "msg_final" }, + { + type: "response.output_item.done", + item: { type: "message", id: "msg_final", phase: "final_answer" }, + }, + { type: "response.completed", response: { id: "resp_1" } }, + ), + ), + ), + ) + + expect(response.events.filter((event) => event.type.startsWith("text-"))).toEqual([ + { + type: "text-start", + id: "msg_commentary", + providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, + }, + { type: "text-delta", id: "msg_commentary", text: "Checking first." }, + { + type: "text-end", + id: "msg_commentary", + providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, + }, + { + type: "text-start", + id: "msg_final", + providerMetadata: { openai: { itemId: "msg_final", phase: "final_answer" } }, + }, + { type: "text-delta", id: "msg_final", text: "Finished." }, + { + type: "text-end", + id: "msg_final", + providerMetadata: { openai: { itemId: "msg_final", phase: "final_answer" } }, + }, + ]) + expect(response.message.content).toEqual([ + { + type: "text", + text: "Checking first.", + providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, + }, + { + type: "text", + text: "Finished.", + providerMetadata: { openai: { itemId: "msg_final", phase: "final_answer" } }, + }, + ]) + }), + ) + + it.effect("preserves phased message and content boundaries", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { + type: "response.output_item.added", + item: { type: "message", id: "msg_commentary", phase: "commentary" }, + }, + { + type: "response.output_text.delta", + item_id: "msg_commentary", + content_index: 0, + delta: "First.", + }, + { + type: "response.output_item.added", + item: { type: "message", id: "msg_commentary" }, + }, + { + type: "response.output_text.done", + item_id: "msg_commentary", + content_index: 0, + text: "First.", + }, + { + type: "response.output_text.done", + item_id: "msg_commentary", + content_index: 1, + text: "Second.", + }, + { + type: "response.output_item.done", + item: { type: "message", id: "msg_commentary" }, + }, + { + type: "response.output_item.added", + item: { type: "message", id: "msg_commentary_2", phase: "commentary" }, + }, + { + type: "response.output_text.delta", + item_id: "msg_commentary_2", + content_index: 0, + delta: "Thi", + }, + { + type: "response.output_text.done", + item_id: "msg_commentary_2", + content_index: 0, + text: "Third.", + }, + { + type: "response.output_item.done", + item: { type: "message", id: "msg_commentary_2", phase: "commentary" }, + }, + { + type: "response.output_item.added", + item: { type: "message", id: "openai-text-0" }, + }, + { + type: "response.output_text.done", + item_id: "openai-text-0", + content_index: 0, + text: "Final.", + }, + { + type: "response.output_item.done", + item: { + type: "message", + id: "openai-text-0", + phase: "final_answer", + content: [ + { + type: "output_text", + text: "Final.", + annotations: [ + { + type: "url_citation", + url: "https://example.com", + title: "Example", + start_index: 0, + end_index: 6, + }, + ], + }, + ], + }, + }, + { + type: "response.output_item.added", + item: { type: "message", id: "msg_null", phase: null }, + }, + { + type: "response.output_text.done", + item_id: "msg_null", + content_index: 0, + text: "Nullable.", + }, + { + type: "response.output_item.done", + item: { type: "message", id: "msg_null", phase: null }, + }, + { + type: "response.output_item.added", + item: { type: "message", id: "msg_unphased" }, + }, + { + type: "response.output_text.done", + item_id: "msg_unphased", + content_index: 0, + text: "Unphased.", + }, + { + type: "response.output_item.done", + item: { type: "message", id: "msg_unphased" }, + }, + { type: "response.completed", response: { id: "resp_1" } }, + ), + ), + ), + ) + + expect(response.message.content).toEqual([ + { + type: "text", + text: "First.", + providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, + }, + { + type: "text", + text: "Second.", + providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, + }, + { + type: "text", + text: "Third.", + providerMetadata: { openai: { itemId: "msg_commentary_2", phase: "commentary" } }, + }, + { + type: "text", + text: "Final.", + providerMetadata: { + openai: { + itemId: "openai-text-0", + phase: "final_answer", + annotations: [ + { + type: "url_citation", + url: "https://example.com", + title: "Example", + start_index: 0, + end_index: 6, + }, + ], + }, + }, + }, + { + type: "text", + text: "Nullable.", + providerMetadata: { openai: { itemId: "msg_null", phase: null } }, + }, + { + type: "text", + text: "Unphased.", + providerMetadata: { openai: { itemId: "msg_unphased" } }, + }, + ]) + + expect(response.events.filter((event) => event.type.startsWith("text-"))).toEqual([ + { + type: "text-start", + id: "msg_commentary", + providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, + }, + { type: "text-delta", id: "msg_commentary", text: "First." }, + { + type: "text-end", + id: "msg_commentary", + providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, + }, + { + type: "text-start", + id: "openai-text-0", + providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, + }, + { type: "text-delta", id: "openai-text-0", text: "Second." }, + { + type: "text-end", + id: "openai-text-0", + providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, + }, + { + type: "text-start", + id: "msg_commentary_2", + providerMetadata: { openai: { itemId: "msg_commentary_2", phase: "commentary" } }, + }, + { type: "text-delta", id: "msg_commentary_2", text: "Thi" }, + { type: "text-delta", id: "msg_commentary_2", text: "rd." }, + { + type: "text-end", + id: "msg_commentary_2", + providerMetadata: { openai: { itemId: "msg_commentary_2", phase: "commentary" } }, + }, + { + type: "text-start", + id: "openai-text-1", + providerMetadata: { openai: { itemId: "openai-text-0" } }, + }, + { type: "text-delta", id: "openai-text-1", text: "Final." }, + { + type: "text-end", + id: "openai-text-1", + providerMetadata: { + openai: { + itemId: "openai-text-0", + phase: "final_answer", + annotations: [ + { + type: "url_citation", + url: "https://example.com", + title: "Example", + start_index: 0, + end_index: 6, + }, + ], + }, + }, + }, + { + type: "text-start", + id: "msg_null", + providerMetadata: { openai: { itemId: "msg_null", phase: null } }, + }, + { type: "text-delta", id: "msg_null", text: "Nullable." }, + { + type: "text-end", + id: "msg_null", + providerMetadata: { openai: { itemId: "msg_null", phase: null } }, + }, + { + type: "text-start", + id: "msg_unphased", + providerMetadata: { openai: { itemId: "msg_unphased" } }, + }, + { type: "text-delta", id: "msg_unphased", text: "Unphased." }, + { + type: "text-end", + id: "msg_unphased", + providerMetadata: { openai: { itemId: "msg_unphased" } }, + }, + ]) + + const prepared = yield* LLMClient.prepare( + LLM.request({ model, messages: [response.message] }), + ) + expect(prepared.body.input).toEqual([ + { + type: "message", + id: "msg_commentary", + status: "completed", + role: "assistant", + phase: "commentary", + content: [ + { type: "output_text", text: "First.", annotations: [] }, + { type: "output_text", text: "Second.", annotations: [] }, + ], + }, + { + type: "message", + id: "msg_commentary_2", + status: "completed", + role: "assistant", + phase: "commentary", + content: [{ type: "output_text", text: "Third.", annotations: [] }], + }, + { + type: "message", + id: "openai-text-0", + status: "completed", + role: "assistant", + phase: "final_answer", + content: [ + { + type: "output_text", + text: "Final.", + annotations: [ + { + type: "url_citation", + url: "https://example.com", + title: "Example", + start_index: 0, + end_index: 6, + }, + ], + }, + ], + }, + { + type: "message", + id: "msg_null", + status: "completed", + role: "assistant", + phase: null, + content: [{ type: "output_text", text: "Nullable.", annotations: [] }], + }, + { + type: "message", + id: "msg_unphased", + status: "completed", + role: "assistant", + content: [{ type: "output_text", text: "Unphased.", annotations: [] }], + }, + ]) + }), + ) + it.effect("parses reasoning summary stream fixtures", () => Effect.gen(function* () { const body = sseEvents( @@ -947,7 +1336,7 @@ describe("OpenAI Responses route", () => { encrypted_content: "encrypted-state", summary: [{ type: "summary_text", text: "Checked the previous diff." }], }, - { role: "assistant", content: [{ type: "output_text", text: "The parser changed." }] }, + { role: "assistant", content: "The parser changed." }, { role: "user", content: [{ type: "input_text", text: "Summarize it." }] }, ], }) @@ -995,13 +1384,69 @@ describe("OpenAI Responses route", () => { ) expect(prepared.body.input).toEqual([ - { role: "assistant", content: [{ type: "output_text", text: "Before." }] }, + { role: "assistant", content: "Before." }, { type: "reasoning", encrypted_content: "encrypted-state", summary: [{ type: "summary_text", text: "Checked order." }], }, - { role: "assistant", content: [{ type: "output_text", text: "After." }] }, + { role: "assistant", content: "After." }, + ]) + }), + ) + + it.effect("round-trips assistant message phases", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + messages: [ + Message.assistant([ + { + type: "text", + text: "Checking first.", + providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, + }, + { + type: "text", + text: "Still checking.", + providerMetadata: { openai: { itemId: "msg_commentary_2", phase: "commentary" } }, + }, + { + type: "text", + text: "Finished.", + providerMetadata: { openai: { itemId: "msg_final", phase: "final_answer" } }, + }, + ]), + ], + }), + ) + + expect(prepared.body.input).toEqual([ + { + type: "message", + id: "msg_commentary", + status: "completed", + role: "assistant", + phase: "commentary", + content: [{ type: "output_text", text: "Checking first.", annotations: [] }], + }, + { + type: "message", + id: "msg_commentary_2", + status: "completed", + role: "assistant", + phase: "commentary", + content: [{ type: "output_text", text: "Still checking.", annotations: [] }], + }, + { + type: "message", + id: "msg_final", + status: "completed", + role: "assistant", + phase: "final_answer", + content: [{ type: "output_text", text: "Finished.", annotations: [] }], + }, ]) }), ) @@ -1120,7 +1565,13 @@ describe("OpenAI Responses route", () => { }, }, }, - { type: "text", text: "The parser changed." }, + { + type: "text", + text: "The parser changed.", + providerMetadata: { + openai: { itemId: "msg_1", phase: "final_answer", status: "completed" }, + }, + }, ]), Message.user("Summarize it."), ], @@ -1131,7 +1582,7 @@ describe("OpenAI Responses route", () => { expect(prepared.body).toMatchObject({ input: [ { role: "user", content: [{ type: "input_text", text: "What changed?" }] }, - { role: "assistant", content: [{ type: "output_text", text: "The parser changed." }] }, + { role: "assistant", content: "The parser changed.", phase: "final_answer" }, { role: "user", content: [{ type: "input_text", text: "Summarize it." }] }, ], store: false, diff --git a/packages/opencode/test/session/llm-native.test.ts b/packages/opencode/test/session/llm-native.test.ts index dd4d9cc174..09b2ffbd16 100644 --- a/packages/opencode/test/session/llm-native.test.ts +++ b/packages/opencode/test/session/llm-native.test.ts @@ -119,7 +119,7 @@ const storedSession = { const openAIResponses = { user: (text: string) => ({ role: "user", content: [{ type: "input_text", text }] }), - assistant: (text: string) => ({ role: "assistant", content: [{ type: "output_text", text }] }), + assistant: (text: string) => ({ role: "assistant", content: text }), openaiReasoning: (text: string, encryptedContent: string) => ({ type: "reasoning", encrypted_content: encryptedContent, From 53669cab2b19815f03c73518fbad0790da31b65b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 24 Jul 2026 21:07:01 +0000 Subject: [PATCH 002/129] chore: generate --- .../round-trips-commentary-into-a-final-answer.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/llm/test/fixtures/recordings/openai-responses-phase/round-trips-commentary-into-a-final-answer.json b/packages/llm/test/fixtures/recordings/openai-responses-phase/round-trips-commentary-into-a-final-answer.json index bbb47c4cc0..c7abea9faf 100644 --- a/packages/llm/test/fixtures/recordings/openai-responses-phase/round-trips-commentary-into-a-final-answer.json +++ b/packages/llm/test/fixtures/recordings/openai-responses-phase/round-trips-commentary-into-a-final-answer.json @@ -3,13 +3,7 @@ "metadata": { "name": "openai-responses-phase/round-trips-commentary-into-a-final-answer", "recordedAt": "2026-07-23T18:55:21.217Z", - "tags": [ - "prefix:openai-responses-phase", - "provider:openai", - "protocol:openai-responses", - "phase", - "tool" - ] + "tags": ["prefix:openai-responses-phase", "provider:openai", "protocol:openai-responses", "phase", "tool"] }, "interactions": [ { From 7840562d1b7ec46bc2beb02e2114ce14f7b9a384 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:31:38 -0500 Subject: [PATCH 003/129] fix(llm): revert response message phases (#38761) --- .../llm/src/protocols/openai-responses.ts | 274 +--------- packages/llm/src/protocols/utils/lifecycle.ts | 19 +- ...-trips-commentary-into-a-final-answer.json | 46 -- ...ponses-gpt-5-5-reasoning-continuation.json | 8 +- .../openai-responses-phase.recorded.test.ts | 89 ---- .../test/provider/openai-responses.test.ts | 467 +----------------- .../opencode/test/session/llm-native.test.ts | 2 +- 7 files changed, 28 insertions(+), 877 deletions(-) delete mode 100644 packages/llm/test/fixtures/recordings/openai-responses-phase/round-trips-commentary-into-a-final-answer.json delete mode 100644 packages/llm/test/provider/openai-responses-phase.recorded.test.ts diff --git a/packages/llm/src/protocols/openai-responses.ts b/packages/llm/src/protocols/openai-responses.ts index 8e2abc9108..4936d31c92 100644 --- a/packages/llm/src/protocols/openai-responses.ts +++ b/packages/llm/src/protocols/openai-responses.ts @@ -46,12 +46,8 @@ type OpenAIResponsesInputContent = Schema.Schema.Type - const OpenAIResponsesReasoningSummaryText = Schema.Struct({ type: Schema.tag("summary_text"), text: Schema.String, @@ -82,19 +78,7 @@ const OpenAIResponsesFunctionCallOutput = Schema.Union([ const OpenAIResponsesInputItem = Schema.Union([ Schema.Struct({ role: Schema.tag("system"), content: Schema.String }), Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }), - Schema.Struct({ - role: Schema.tag("assistant"), - content: Schema.String, - phase: optionalNull(OpenAIResponsesMessagePhase), - }), - Schema.Struct({ - type: Schema.tag("message"), - id: Schema.String, - status: Schema.Literals(["in_progress", "completed", "incomplete"]), - role: Schema.tag("assistant"), - content: Schema.Array(OpenAIResponsesOutputText), - phase: optionalNull(OpenAIResponsesMessagePhase), - }), + Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }), OpenAIResponsesReasoningItem, OpenAIResponsesItemReference, Schema.Struct({ @@ -210,9 +194,7 @@ const OpenAIResponsesStreamItem = Schema.Struct({ server_label: Schema.optional(Schema.String), output: Schema.optional(Schema.Unknown), error: Schema.optional(Schema.Unknown), - content: Schema.optional(Schema.Array(Schema.Unknown)), encrypted_content: optionalNull(Schema.String), - phase: optionalNull(OpenAIResponsesMessagePhase), }) type OpenAIResponsesStreamItem = Schema.Schema.Type @@ -230,9 +212,7 @@ const OpenAIResponsesErrorPayload = Schema.Struct({ const OpenAIResponsesEvent = Schema.Struct({ type: Schema.String, delta: Schema.optional(Schema.String), - text: Schema.optional(Schema.String), item_id: Schema.optional(Schema.String), - content_index: Schema.optional(Schema.Number), summary_index: Schema.optional(Schema.Number), item: Schema.optional(OpenAIResponsesStreamItem), response: Schema.optional( @@ -257,18 +237,10 @@ interface ParserState { readonly tools: ToolStream.State readonly hasFunctionCall: boolean readonly lifecycle: Lifecycle.State - readonly messageItems: Readonly> - readonly messageContentIDs: ReadonlySet - readonly nextMessageContentID: number readonly reasoningItems: Readonly> readonly store: boolean | undefined } -interface MessageStreamItem { - readonly providerMetadata?: ProviderMetadata - readonly content: Readonly> -} - type ReasoningSummaryStatus = "active" | "can-conclude" | "concluded" interface ReasoningStreamItem { @@ -326,26 +298,6 @@ const lowerReasoning = (part: ReasoningPart): OpenAIResponsesReasoningInput | un } } -const messagePhase = (part: TextPart): OpenAIResponsesMessagePhase | null | undefined => { - const phase = part.providerMetadata?.openai?.phase - return phase === "commentary" || phase === "final_answer" || phase === null ? phase : undefined -} - -const messageItemID = (part: TextPart) => { - const itemID = part.providerMetadata?.openai?.itemId - return typeof itemID === "string" && itemID.length > 0 ? itemID : undefined -} - -const messageStatus = (part: TextPart) => { - const status = part.providerMetadata?.openai?.status - return status === "in_progress" || status === "completed" || status === "incomplete" ? status : undefined -} - -const messageAnnotations = (part: TextPart) => { - const annotations = part.providerMetadata?.openai?.annotations - return Array.isArray(annotations) ? annotations : [] -} - const hostedToolItemID = (part: ToolResultPart) => { const openai = part.providerMetadata?.openai return ProviderShared.isRecord(openai) && typeof openai.itemId === "string" && openai.itemId.length > 0 @@ -416,49 +368,17 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ } if (message.role === "assistant") { - const inputStart = input.length const content: TextPart[] = [] - let phase: OpenAIResponsesMessagePhase | null | undefined - let itemID: string | undefined - let status: "in_progress" | "completed" | "incomplete" | undefined const reasoningItems: Record = {} const reasoningReferences = new Set() const hostedToolReferences = new Set() const flushText = () => { if (content.length === 0) return - input.push( - itemID - ? { - type: "message", - id: itemID, - status: status ?? "completed", - role: "assistant", - content: content.map((part) => ({ - type: "output_text", - text: part.text, - annotations: messageAnnotations(part), - })), - ...(phase !== undefined ? { phase } : {}), - } - : { - role: "assistant", - content: ProviderShared.joinText(content), - ...(phase !== undefined ? { phase } : {}), - }, - ) + input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) }) content.splice(0, content.length) - phase = undefined - itemID = undefined - status = undefined } for (const part of message.content) { if (part.type === "text") { - const nextPhase = messagePhase(part) - const nextItemID = messageItemID(part) - if (content.length > 0 && (phase !== nextPhase || itemID !== nextItemID)) flushText() - phase = nextPhase - itemID = nextItemID - status = messageStatus(part) ?? status content.push(part) continue } @@ -509,20 +429,6 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ ]) } flushText() - if (store === false && Object.values(reasoningItems).some((item) => typeof item.encrypted_content !== "string")) - input.splice( - inputStart, - input.length - inputStart, - ...input.slice(inputStart).map((item) => - "type" in item && item.type === "message" - ? { - role: "assistant" as const, - content: ProviderShared.joinText(item.content), - ...(item.phase !== undefined ? { phase: item.phase } : {}), - } - : item, - ), - ) continue } @@ -706,132 +612,13 @@ const NO_EVENTS: StepResult["1"] = [] // the protocol's `terminal` predicate stay in sync. const TERMINAL_TYPES = new Set(["response.completed", "response.incomplete", "response.failed"]) -const messageMetadata = (item: OpenAIResponsesStreamItem, id: string, previous?: ProviderMetadata) => { - const openai = previous?.openai - const phase = item.phase !== undefined ? item.phase : openai?.phase - const status = - item.status === "in_progress" || item.status === "completed" || item.status === "incomplete" - ? item.status - : openai?.status - return openaiMetadata({ - itemId: id, - ...(phase === "commentary" || phase === "final_answer" || phase === null ? { phase } : {}), - ...(status === "in_progress" || status === "completed" || status === "incomplete" ? { status } : {}), - }) -} - -const messageContentMetadata = ( - providerMetadata: ProviderMetadata, - item: OpenAIResponsesStreamItem, - index: number, -): ProviderMetadata => { - const content = item.content?.[index] - if (!ProviderShared.isRecord(content) || content.type !== "output_text" || !Array.isArray(content.annotations)) - return providerMetadata - return openaiMetadata({ ...providerMetadata.openai, annotations: content.annotations }) -} - -const ensureMessageContent = (state: ParserState, event: OpenAIResponsesEvent) => { - const itemID = event.item_id ?? "text-0" - const index = event.content_index ?? 0 - const item = state.messageItems[itemID] ?? { content: {} } - const existing = item.content[index] - if (existing) return { state, itemID, index, item, content: existing } - const findID = (next: number): readonly [string, number] => { - const id = `openai-text-${next}` - return state.messageContentIDs.has(id) ? findID(next + 1) : [id, next + 1] - } - const [id, nextMessageContentID] = - index === 0 && !state.messageContentIDs.has(itemID) - ? ([itemID, state.nextMessageContentID] as const) - : findID(state.nextMessageContentID) - const content = { id, text: "" } - const nextItem = { ...item, content: { ...item.content, [index]: content } } - return { - state: { - ...state, - messageItems: { ...state.messageItems, [itemID]: nextItem }, - messageContentIDs: new Set([...state.messageContentIDs, id]), - nextMessageContentID, - }, - itemID, - index, - item: nextItem, - content, - } -} - -const updateMessageContent = ( - state: ParserState, - itemID: string, - index: number, - content: { readonly id: string; readonly text: string }, -): ParserState => ({ - ...state, - messageItems: { - ...state.messageItems, - [itemID]: { - ...state.messageItems[itemID], - content: { ...state.messageItems[itemID]?.content, [index]: content }, - }, - }, -}) - -const closeOtherMessageContent = (state: ParserState, events: LLMEvent[], item: MessageStreamItem, index: number) => - Object.entries(item.content).reduce( - (lifecycle, entry) => - Number(entry[0]) === index ? lifecycle : Lifecycle.textEnd(lifecycle, events, entry[1].id, item.providerMetadata), - state.lifecycle, - ) - -const appendOutputText = (state: ParserState, event: OpenAIResponsesEvent, text: string): StepResult => { - const ensured = ensureMessageContent(state, event) - const events: LLMEvent[] = [] - const lifecycle = Lifecycle.textStart( - closeOtherMessageContent(ensured.state, events, ensured.item, ensured.index), - events, - ensured.content.id, - ensured.item.providerMetadata, - ) - return [ - { - ...updateMessageContent(ensured.state, ensured.itemID, ensured.index, { - ...ensured.content, - text: ensured.content.text + text, - }), - lifecycle: Lifecycle.textDelta(lifecycle, events, ensured.content.id, text), - }, - events, - ] -} - const onOutputTextDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { if (!event.delta) return [state, NO_EVENTS] - return appendOutputText(state, event, event.delta) -} - -const onOutputTextDone = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { - if (event.text === undefined) return [state, NO_EVENTS] - const ensured = ensureMessageContent(state, event) - if (event.text === ensured.content.text) { - if (ensured.state.lifecycle.text.has(ensured.content.id)) return [ensured.state, NO_EVENTS] - const events: LLMEvent[] = [] - return [ - { - ...ensured.state, - lifecycle: Lifecycle.textStart( - closeOtherMessageContent(ensured.state, events, ensured.item, ensured.index), - events, - ensured.content.id, - ensured.item.providerMetadata, - ), - }, - events, - ] - } - if (event.text.startsWith(ensured.content.text)) - return appendOutputText(ensured.state, event, event.text.slice(ensured.content.text.length)) - return [ensured.state, NO_EVENTS] + const events: LLMEvent[] = [] + return [ + { ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, event.item_id ?? "text-0", event.delta) }, + events, + ] } const onReasoningDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { @@ -868,23 +655,6 @@ const reasoningMetadata = (item: OpenAIResponsesStreamItem & { id: string }) => // best-effort, not guaranteed. const onOutputItemAdded = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { const item = event.item - if (item?.type === "message" && item.id) { - const existing = state.messageItems[item.id] - return [ - { - ...state, - messageItems: { - ...state.messageItems, - [item.id]: { - ...existing, - providerMetadata: messageMetadata(item, item.id, existing?.providerMetadata), - content: existing?.content ?? {}, - }, - }, - }, - NO_EVENTS, - ] - } if (item && isReasoningItem(item)) { const events: LLMEvent[] = [] return [ @@ -1042,32 +812,6 @@ const onOutputItemDone = Effect.fn("OpenAIResponses.onOutputItemDone")(function* const item = event.item if (!item) return [state, NO_EVENTS] satisfies StepResult - if (item.type === "message" && item.id) { - const events: LLMEvent[] = [] - const itemID = item.id - const messageItem = state.messageItems[itemID] - const { [itemID]: _finished, ...messageItems } = state.messageItems - const providerMetadata = messageMetadata(item, itemID, messageItem?.providerMetadata) - const lifecycle = Object.entries(messageItem?.content ?? {}).reduce( - (lifecycle, entry) => - Lifecycle.textEnd( - lifecycle, - events, - entry[1].id, - messageContentMetadata(providerMetadata, item, Number(entry[0])), - ), - state.lifecycle, - ) - return [ - { - ...state, - lifecycle, - messageItems, - }, - events, - ] satisfies StepResult - } - if (item.type === "function_call") { if (!item.id || !item.call_id || !item.name) return [state, NO_EVENTS] satisfies StepResult const tools = state.tools[item.id] @@ -1195,7 +939,6 @@ const step = (state: ParserState, event: OpenAIResponsesEvent) => { if (event.type === "response.reasoning_summary_part.done") return Effect.succeed(onReasoningSummaryPartDone(state, event)) if (event.type === "response.output_item.added") return Effect.succeed(onOutputItemAdded(state, event)) - if (event.type === "response.output_text.done") return Effect.succeed(onOutputTextDone(state, event)) if (event.type === "response.function_call_arguments.delta") return onFunctionCallArgumentsDelta(state, event) if (event.type === "response.output_item.done") return onOutputItemDone(state, event) if (event.type === "response.completed" || event.type === "response.incomplete") @@ -1225,9 +968,6 @@ export const protocol = Protocol.make({ hasFunctionCall: false, tools: ToolStream.empty(), lifecycle: Lifecycle.initial(), - messageItems: {}, - messageContentIDs: new Set(), - nextMessageContentID: 0, reasoningItems: {}, store: OpenAIOptions.store(request), }), diff --git a/packages/llm/src/protocols/utils/lifecycle.ts b/packages/llm/src/protocols/utils/lifecycle.ts index 8add02ce67..eb6c95dfbd 100644 --- a/packages/llm/src/protocols/utils/lifecycle.ts +++ b/packages/llm/src/protocols/utils/lifecycle.ts @@ -14,17 +14,14 @@ export const stepStart = (state: State, events: LLMEvent[]): State => { return { ...state, stepStarted: true } } -export const textStart = (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata): State => { - if (state.text.has(id)) return state - const stepped = stepStart(state, events) - events.push(LLMEvent.textStart({ id, ...(providerMetadata ? { providerMetadata } : {}) })) - return { ...stepped, text: new Set([...stepped.text, id]) } -} - export const textDelta = (state: State, events: LLMEvent[], id: string, text: string): State => { - const started = textStart(state, events, id) - events.push(LLMEvent.textDelta({ id, text })) - return started + const stepped = stepStart(state, events) + if (stepped.text.has(id)) { + events.push(LLMEvent.textDelta({ id, text })) + return stepped + } + events.push(LLMEvent.textStart({ id }), LLMEvent.textDelta({ id, text })) + return { ...stepped, text: new Set([...stepped.text, id]) } } export const reasoningStart = ( @@ -68,7 +65,7 @@ export const reasoningEnd = ( export const textEnd = (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata): State => { if (!state.text.has(id)) return state const stepped = stepStart(state, events) - events.push(LLMEvent.textEnd({ id, ...(providerMetadata ? { providerMetadata } : {}) })) + events.push(LLMEvent.textEnd({ id, providerMetadata })) const text = new Set(stepped.text) text.delete(id) return { ...stepped, text } diff --git a/packages/llm/test/fixtures/recordings/openai-responses-phase/round-trips-commentary-into-a-final-answer.json b/packages/llm/test/fixtures/recordings/openai-responses-phase/round-trips-commentary-into-a-final-answer.json deleted file mode 100644 index c7abea9faf..0000000000 --- a/packages/llm/test/fixtures/recordings/openai-responses-phase/round-trips-commentary-into-a-final-answer.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "version": 1, - "metadata": { - "name": "openai-responses-phase/round-trips-commentary-into-a-final-answer", - "recordedAt": "2026-07-23T18:55:21.217Z", - "tags": ["prefix:openai-responses-phase", "provider:openai", "protocol:openai-responses", "phase", "tool"] - }, - "interactions": [ - { - "transport": "http", - "request": { - "method": "POST", - "url": "https://api.openai.com/v1/responses", - "headers": { - "content-type": "application/json" - }, - "body": "{\"model\":\"gpt-5.6-sol\",\"input\":[{\"role\":\"system\",\"content\":\"Before calling get_weather, briefly tell the user you are checking. Then call get_weather exactly once. Do not provide the final answer until its result is available.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":100,\"stream\":true}" - }, - "response": { - "status": 200, - "headers": { - "content-type": "text/event-stream; charset=utf-8" - }, - "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_09a288a54615e317016a6263962f6081908c42f100432d7432\",\"object\":\"response\",\"created_at\":1784832918,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":100,\"max_tool_calls\":null,\"model\":\"gpt-5.6-sol\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Get current weather for a city.\",\"name\":\"get_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_09a288a54615e317016a6263962f6081908c42f100432d7432\",\"object\":\"response\",\"created_at\":1784832918,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":100,\"max_tool_calls\":null,\"model\":\"gpt-5.6-sol\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Get current weather for a city.\",\"name\":\"get_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"commentary\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"I\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"TrlggQ3HsX40yjX\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"’ll\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"OT4APBDnZPskz\",\"output_index\":0,\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" check\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"y6OCrBSFwy\",\"output_index\":0,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" the\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"VgUHMFuGRsCe\",\"output_index\":0,\"sequence_number\":7}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" current\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"NfRf0E2g\",\"output_index\":0,\"sequence_number\":8}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" weather\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"FDKK7SxE\",\"output_index\":0,\"sequence_number\":9}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" in\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"VpCRtSlIHUKRx\",\"output_index\":0,\"sequence_number\":10}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" Paris\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"7ksv7FLpVR\",\"output_index\":0,\"sequence_number\":11}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".\",\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"obfuscation\":\"lo7fPMYb2XF2iIz\",\"output_index\":0,\"sequence_number\":12}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":13,\"text\":\"I’ll check the current weather in Paris.\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"I’ll check the current weather in Paris.\"},\"sequence_number\":14}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"I’ll check the current weather in Paris.\"}],\"phase\":\"commentary\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":15}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_sp8Ji4cqPjnpRbTQ8Kss4epd\",\"name\":\"get_weather\"},\"output_index\":1,\"sequence_number\":16}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"obfuscation\":\"j3afFo6Txi12z5\",\"output_index\":1,\"sequence_number\":17}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"city\",\"item_id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"obfuscation\":\"k5BeAAvHaqNo\",\"output_index\":1,\"sequence_number\":18}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"obfuscation\":\"p7WgYInwBJjJs\",\"output_index\":1,\"sequence_number\":19}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"Paris\",\"item_id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"obfuscation\":\"Gm7YVJP56OJ\",\"output_index\":1,\"sequence_number\":20}\n\nevent: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\"}\",\"item_id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"obfuscation\":\"DP3YnNaUFF1AFQ\",\"output_index\":1,\"sequence_number\":21}\n\nevent: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\",\"item_id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"output_index\":1,\"sequence_number\":22}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\",\"call_id\":\"call_sp8Ji4cqPjnpRbTQ8Kss4epd\",\"name\":\"get_weather\"},\"output_index\":1,\"sequence_number\":23}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_09a288a54615e317016a6263962f6081908c42f100432d7432\",\"object\":\"response\",\"created_at\":1784832918,\"status\":\"completed\",\"background\":false,\"completed_at\":1784832920,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":100,\"max_tool_calls\":null,\"model\":\"gpt-5.6-sol\",\"moderation\":null,\"output\":[{\"id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"I’ll check the current weather in Paris.\"}],\"phase\":\"commentary\",\"role\":\"assistant\"},{\"id\":\"fc_09a288a54615e317016a62639805948190a1aad87870887193\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\",\"call_id\":\"call_sp8Ji4cqPjnpRbTQ8Kss4epd\",\"name\":\"get_weather\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Get current weather for a city.\",\"name\":\"get_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":86,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":34,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":120},\"user\":null,\"metadata\":{}},\"sequence_number\":24}\n\n" - } - }, - { - "transport": "http", - "request": { - "method": "POST", - "url": "https://api.openai.com/v1/responses", - "headers": { - "content-type": "application/json" - }, - "body": "{\"model\":\"gpt-5.6-sol\",\"input\":[{\"role\":\"system\",\"content\":\"Before calling get_weather, briefly tell the user you are checking. Then call get_weather exactly once. After its result, answer exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]},{\"type\":\"message\",\"id\":\"msg_09a288a54615e317016a626397d4d48190b8e2dac34ada4601\",\"status\":\"completed\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"I’ll check the current weather in Paris.\",\"annotations\":[]}],\"phase\":\"commentary\"},{\"type\":\"function_call\",\"call_id\":\"call_sp8Ji4cqPjnpRbTQ8Kss4epd\",\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_sp8Ji4cqPjnpRbTQ8Kss4epd\",\"output\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":100,\"stream\":true}" - }, - "response": { - "status": 200, - "headers": { - "content-type": "text/event-stream; charset=utf-8" - }, - "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_09a288a54615e317016a626398499481908cda8e1ed0ef45bd\",\"object\":\"response\",\"created_at\":1784832920,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":100,\"max_tool_calls\":null,\"model\":\"gpt-5.6-sol\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Get current weather for a city.\",\"name\":\"get_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_09a288a54615e317016a626398499481908cda8e1ed0ef45bd\",\"object\":\"response\",\"created_at\":1784832920,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":100,\"max_tool_calls\":null,\"model\":\"gpt-5.6-sol\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Get current weather for a city.\",\"name\":\"get_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"Paris\",\"item_id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"logprobs\":[],\"obfuscation\":\"SQK4VKpPg6o\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" is\",\"item_id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"logprobs\":[],\"obfuscation\":\"1kFOJRFyao7zE\",\"output_index\":0,\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" sunny\",\"item_id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"logprobs\":[],\"obfuscation\":\"n4cE1OQJnX\",\"output_index\":0,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".\",\"item_id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"logprobs\":[],\"obfuscation\":\"wd9OLc2GBwngpGN\",\"output_index\":0,\"sequence_number\":7}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":8,\"text\":\"Paris is sunny.\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Paris is sunny.\"},\"sequence_number\":9}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Paris is sunny.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":10}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_09a288a54615e317016a626398499481908cda8e1ed0ef45bd\",\"object\":\"response\",\"created_at\":1784832920,\"status\":\"completed\",\"background\":false,\"completed_at\":1784832921,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":100,\"max_tool_calls\":null,\"model\":\"gpt-5.6-sol\",\"moderation\":null,\"output\":[{\"id\":\"msg_09a288a54615e317016a626398f9f88190ba4b9ecc8b29fd03\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Paris is sunny.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"all_turns\",\"effort\":\"medium\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Get current weather for a city.\",\"name\":\"get_weather\",\"output_schema\":null,\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":140,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":8,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":148},\"user\":null,\"metadata\":{}},\"sequence_number\":11}\n\n" - } - } - ] -} diff --git a/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json b/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json index cb028796f3..47670c8127 100644 --- a/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json +++ b/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json @@ -2,7 +2,7 @@ "version": 1, "metadata": { "name": "openai-responses/openai-responses-gpt-5-5-reasoning-continuation", - "recordedAt": "2026-07-23T18:57:01.137Z", + "recordedAt": "2026-05-23T23:19:06.776Z", "provider": "openai", "route": "openai-responses", "transport": "http", @@ -33,7 +33,7 @@ "headers": { "content-type": "text/event-stream; charset=utf-8" }, - "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_05f4a9bec03f8148016a6263faf3bc81959b717b99f194a37c\",\"object\":\"response\",\"created_at\":1784833018,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_05f4a9bec03f8148016a6263faf3bc81959b717b99f194a37c\",\"object\":\"response\",\"created_at\":1784833018,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"rs_05f4a9bec03f8148016a6263fb83748195b13a0e6b77c0dd88\",\"type\":\"reasoning\",\"content\":[],\"encrypted_content\":\"gAAAAABqYmP7gx69qnBLNDRIOVvsxN78GS99z9MYWR_FTPUwnVyq8J470WTXQWQEqp9PF8tLyswAhxg2PkxL7ijhN9190IV6QR1waQUzNd6NLYDc5-GG8mJ0LdKYPABZqFYRWV6MEJhDW2CrR5XiGErIzIlMCWEBkE79DDmLOdyOEVMTtetK_CUuxbBGnQ2_8_16FP8AXCqL6xKBCzFxTsQDa9oKTuMoS7jczBlC71fWBiw_cEfILOIUe7_5K4ze7MJG079Ty580gZCAQ0vteMnKpPKSfugMhKlVB_9Wn3wofeL-Xf8s1QojpIAUgHE_fh9fMNGeMkEsUFKhz_vOQrU7FH3NpGTYs0qKQVJaX9FHrR5EooMMimg2TFqc3GZkHJt9VJe1dhfNzcnybIdyz5s0Y6l-1FSD90_8mHR3ZUdfgOBYrsDfVvXpCUdyn4lQGJMkJYgBqAZpXZHvRw16t9_6stCvsScdb5nGrat54qbSYx1mfXD9S2RPw57vzcnClSpFMzen9oMzPWnBxjQujMzjauz_Ps4UJ_H59XUcvuL8fzWv00qhVvbL59NCMpQFdY0b4TtDuIKT0tgEHRKiHMN4zl3zoVJ9bp8qsxVTBBkIF3Q-Wsf4655dkD3yV0H66Yp3XAO5hvRO-0y8FMMpWcc0N5liLOMKiCKj9XMeGi2p5nF66vkPs3fm880W3S2fyOVsxbTWF_TVDJ1SDaHn_xPzzYQCg047sGOQ9S6jvAfblBqIO9iOV3zOF-Gu5Zv9gbJEywTIPSx_u8sKNAqvlxpZ6zJLOyjipByUvFb27MYJLdPw9fIV9w5D2XVwowB8Bg13joWKgm5ne75-VudmIlm-AoWC78zT2_WIKpIN0jYhJDOgxIgpFeI4LEGKh6BlVSQ0-CRrOGw1FnsxKfGN0FL9ChLs-rz4yA==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"rs_05f4a9bec03f8148016a6263fb83748195b13a0e6b77c0dd88\",\"type\":\"reasoning\",\"content\":[],\"encrypted_content\":\"gAAAAABqYmP8aJHmIOttZSqCfZrRqPkxHEGRKL8agRHsomAtpHOM91zNm-YwW-Tupi5CFHy3SFlIatT1_VVZLSR50Mf1jZezXHUkIdB003BGPUwaeHlhlq3B63dukCmAGJso8xJvDw0YqYppy2FXNIoPoys3wI4n8LSTiUOm6yXEyUkaBt4sXLeWsg_NLqYo-ilmv60ppKV1vPQNZkHAX2N0PCZebSmzto5a896xV7jkKVsEMhanPnKP8N3G_ag_-B4ZPr01IAma4xJ2jXysRX_P-zN7AqGDRxBuuoIzylLizo68wNH9cXmnvQcCEkjKxOTpmQGCl0LxSnK_UnaLwErtj9FUMXDaDqbfugCZrCy5pUq0RgBSoAnZvOqH6pWpXPvclQI9Wsg-Ra3bv8sYtf2qYBMebaNIbI_BVVKN8Lrpj7UWODiRZImgfUWHuflaf98H1oD09f7e55gg7GlQ0r6EVZwbuX3GaEwsHbDe7KdsPmwV_RV_ZQpxm14aBypGtapXEAd8SMJxygZtuQKCTJuOwsgZug_I1cGw4EF5AIg5EWzVhvJmlzPrBqas-VdL0_VIfohJAm9181eYKX5ITeUZkdrNpwk7yMCLG4yHCOiP_x33Gyz3xOOUzNSVMy56F7f_K1Bd-eqHK4zMsYrS4-c2cbC5HPPNUUFh8-CaMaJ-34P2TvTHrxR9QY6PXMyfiEMhNC8Mpblhh3ShCvNWTAsrujWHmtDRp47cSNsiNxTSRMU75OsDviGcnuNCZDHBA-92N9g-nPqBvqJfFJXsHbpNzLRCgkv3O8ZOvMPOLLM4OZu1fELgAdKqWNupZgys6n1pUVsRGctECeVZ4NOafuvHvm6wUHRXuLLUN6ATJJ4RdSD_DAtnGD1QFstXwEeBHgBwMMppaIgBvZGbz5skrDm5NBv7AEdCl6UzZ-0zWP6AFxowTzTNuHUyIO-Buwvq_hmLn2hemhr0wOBBgTqFcW51cnybHt7sQD6AhGYlLjNnGu5XH7qcdTY=\",\"summary\":[]},\"output_index\":0,\"sequence_number\":3}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_05f4a9bec03f8148016a6263fc25a08195b4a994aeeed62684\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":4}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_05f4a9bec03f8148016a6263fc25a08195b4a994aeeed62684\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"Hello\",\"item_id\":\"msg_05f4a9bec03f8148016a6263fc25a08195b4a994aeeed62684\",\"logprobs\":[],\"obfuscation\":\"l9gChAJHAvW\",\"output_index\":1,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"!\",\"item_id\":\"msg_05f4a9bec03f8148016a6263fc25a08195b4a994aeeed62684\",\"logprobs\":[],\"obfuscation\":\"MgDcWbMrxjIR4h6\",\"output_index\":1,\"sequence_number\":7}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_05f4a9bec03f8148016a6263fc25a08195b4a994aeeed62684\",\"logprobs\":[],\"output_index\":1,\"sequence_number\":8,\"text\":\"Hello!\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_05f4a9bec03f8148016a6263fc25a08195b4a994aeeed62684\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"},\"sequence_number\":9}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_05f4a9bec03f8148016a6263fc25a08195b4a994aeeed62684\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":10}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_05f4a9bec03f8148016a6263faf3bc81959b717b99f194a37c\",\"object\":\"response\",\"created_at\":1784833018,\"status\":\"completed\",\"background\":false,\"completed_at\":1784833020,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[{\"id\":\"rs_05f4a9bec03f8148016a6263fb83748195b13a0e6b77c0dd88\",\"type\":\"reasoning\",\"content\":[],\"encrypted_content\":\"gAAAAABqYmP8gH5Qc4oBXBDbJyFBgvZVodKHctAOFeZp3khLa37IbJljUkbx7ERw0pgedvhUBaQsRKB31czAty8Ruv9PJVbyoSUpVamjmVM0-FD9lHpwNDnLPjhjPzcAxQosXeVGuWp6jxwmKjzIowwLFp7ImRG2syV-2-QqZ6Wef4VuPzmQ6RVVQUeqBz5Pvhf0FKkqzgb9T3vv0bV-V8qFsdbNKGtQvKKciR5fL6YaUlmY0gsru8UDabWjNjL8oyjZI0HezNcvYNLfmmzptyYpfw208LFiTeD8ntf6xClGgbBztiMQDQhOjQoeaZrrOCaB3iqxbe-_x8xAMRdUpS9NmhJ8oHH1w4JaPQ-Gjvzrrjq0kqpVYXHmdZ-cmwPpRG_0mh4yHkIUYXb0cvoaMyKWLXGcVf3OWLmkEZCa0GuOvwKz8QaPoOAer0L-GrV4p1GadjEUG07EvwsSRyG3fPckMJsSLxW70NNipaGmJys-ihR5YU5qqz74EnpJpYFJnh7Anl5ETl92-ZA4tqAevrW2dwoCNIfGkuX7GMrpPLxTs5sZ1UQWppPyfji-f0YY7198_Ypf8XZfWhB46HYipjSwqErvUyHpE6UpkqWQwwX5wa29clrUrjxTnn1qiw0G2PgvKziwBpImhfWDRGafirQ9G4lh1I8GS5K7vDPVtOPnzxFccYFX4oB5NgSdADMtm5cFKgAqVNM4l04E2FMYJvPr5qn79EWf9AwfOFIuwwNE_sWSkN8VU1-iob0_03iMJ_U2lbT9BT65aJhhSmFUfaqyliC9rUwnFXYEgoFC7jAZ6_LhNJ4ONM26KPUhGu0Uv5xq3aAyk_EKQ_quvYF7euqijnIAzjk8rsV7w7_Qj0Ujnoz7M7QSJx5VaGUCXR8O_pKBcBVLayBe957WZ154qfTYIK96FyOcqM5_RGHlvvfVRi3pnbrQR_odU1e2pAL5hdCyvKtZ3GtN0kaGU1wJOersW5QCUyTIJ9H9ax-YJn3_4Xe4PKKEr5Q=\",\"summary\":[]},{\"id\":\"msg_05f4a9bec03f8148016a6263fc25a08195b4a994aeeed62684\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":31,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":21,\"output_tokens_details\":{\"reasoning_tokens\":13},\"total_tokens\":52},\"user\":null,\"metadata\":{}},\"sequence_number\":11}\n\n" + "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e74e148195beb46e1925d20292\",\"object\":\"response\",\"created_at\":1779578343,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e74e148195beb46e1925d20292\",\"object\":\"response\",\"created_at\":1779578343,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"rs_0a0794dab3b8ec7d016a1235e7ce3881958a5eca32a36a14c5\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXnglldg7hhpTBATVqj7sThK5ATieOVR8sZGYPDW2zYopwpKxA3RyRccK_FPjRvvlzrvL-FitOxmdMGBaKa5jncrT9hHo5IMhsFsCEHkQ1x5tlrKPqtfwJ_LFexR0h_IpPogu8wlVAkHRoWQoq61o9vBxjMOEsq6dtXu09959gXnAvJA3jN_mqNkRZ7Yp6LaJJtLDAAtt_dhX8veoEFXZ412lCY4zcaMvC5o0yq6MPvLIN4NhHmfPKkVAy-j8wGlgA42KR4wd5-VeFXUdeSn32dlNLZZxBFa9w6iTgCQ9aF-3C7RB4OXeSY782QUD1dRyFybd7vJtjlptwXBntSHZ9wugoKSDEj0KnvQKG_WiCWuJvkGiOVno4MAs5QnCmKBnpak5OV1wOhPwX2ez6OmAYT4mMKIogdfivVvUxMrmdVJzgE85WoZEAU2ZporxVXkI7_8p0L6dxxwk_IKiKSCz-bZgsCtOP5Jsr5GeI831nVv272kZ3DugV-hcjGHAE5T9KhebzpFjsdxnJcfxuGY8SyRaLlUAHM_37H4veHsOzyhCoaG8mMaT3gIb4tAvM7ezd1xzLsFae89P5xCv_fNeoV7qmf2IWDWUi1vitIib5w9jsclWRqYaLVZR0GK6dYyNJ1DXDOOcWRdH7UJakv1m2koUbcYWBuxao7sc-af_9ySKAloWhb6QjiVElJHYtwraJBtX-CLBVHEYqAXmZgMUWVbz8NNRA6JS1TrOys7_LiQtXXubLWas_66LyaqmB-628LCUitUISYYc2wmq1uUm7gjPA53Wm4F7VU6g-PO7bt1O0Nd-jasisPXINTX3Z4hgC1APPEq29iEHwmEPnicO_Nu6U3JLfq4DD6r1oLK-RnIp3Ratw0P-Gwog86RBLGUWIEIKdFu6m9d1TI8rIBbAVaBA==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"rs_0a0794dab3b8ec7d016a1235e7ce3881958a5eca32a36a14c5\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXoGMCw3WDXpoD9151PEr2Lt8raW7KBKefQhZJGWx5f8jy152bApO6oE-Mr1BhUtfZNq3OPBVfSL4ioQ9bHREfujIBXgk9LUDBAz2Sle7KjOr9HaUV16A4HBiaFIRFjsHPS9G8yEySp1m6F1CD_WR6apyUGgugRh_y39EcOJmxPOzmiac5DVM6fraA1VpcGbqrZ1x2ANHFDOfnYTycPtPNTgzE7LjkYjDDWbT03uN1YxfP4pqjDVRzY14pA8bSZ8ys-pDv5kUFCAsw-OlU4jYKUXp-M8_6KTaRQP71LPwppt__zG_NJPfy-qUil4pOU8_NoxtxerHgLLXbfExZdzfpoGinoEjn7nj7BJDEtl-LNeNEb5c-1ZymNfVMp-Cs3fLEPkAV8rtHFtZ0MhE_07GKbGo7hTrOmkM4DydxmHsdWGNbXAG35cprslEA5P7p3GHFKnRs5hGs2eq-XcZ3yki64ZBOU_Tv6UR7nUH09gF1rdrJo3dpre6M00COwwdZ02zUP5KxCuI8FKu2jsZu9zgMVXDALsdtM5orTCVLXsn4rddWd111zE-vMjNmMMmktW2cHMjH7j1ooA-9P083koNVYiLi4UhMA64gTqgyl8MxkZekl7eFSMa7qk295NaHOKtFxzYYcZ9jdioCwSPSZ0ZZWLoNgrK7SWfRh0uaTHNcMZ3wq8ae6CguktIeVTCPTQAqJLQqd7AU0oOCKCJ7BWnC-L8UC6m7Pm9ZS958uUVeWBhgKHzMAGq9UeQB7IEeAcbMn3EDgOSfd8qCb8iwU9iG9dcu9axQwWU7pd7kd-T-He61W7z5wWgpx1KehWCxrN6kuKSo6p-uUfwVnJukreOn8BJNAzADQgz68bhmN9VGih7YcKVnLgwDwKditrjSd6-tfE0Baarj3jWENvT6ohY17R9FDrKS-2v8IIX6tGjoKJw8SRhaWLNv4vWlmxRgR0gdac3qumd0GKqsWSveNz01naA==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":3}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":4}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"Hello\",\"item_id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"logprobs\":[],\"obfuscation\":\"3nRhhCWA1H8\",\"output_index\":1,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"!\",\"item_id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"logprobs\":[],\"obfuscation\":\"60NqChSEyXHKsoy\",\"output_index\":1,\"sequence_number\":7}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"logprobs\":[],\"output_index\":1,\"sequence_number\":8,\"text\":\"Hello!\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"},\"sequence_number\":9}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":10}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e74e148195beb46e1925d20292\",\"object\":\"response\",\"created_at\":1779578343,\"status\":\"completed\",\"background\":false,\"completed_at\":1779578344,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[{\"id\":\"rs_0a0794dab3b8ec7d016a1235e7ce3881958a5eca32a36a14c5\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXoMO9Ci_Q06HKQ0YDBarUvbp9ulkR9W2RXWPbx7XKokNCrKUZX-pPPGpUg6r-vTXe8iEX-oED6TmjxZV_nyo838x6pmQJlDqz5JECs2axIrUbCjv9xBt3ob8eAyOizhKFjp3dJNu4i01c38MPZ5QYpD24uCKf69jzjUfydKIEjbo0VhP3K6SDG0V9ZUtua-e6WMqzIg-W5Zs3u64DxGw974ntmvNsx8lsuLR-bk9S5ZZ7zPlCG2Emwfph8UE5HJmIfmMxYlrY5qmXSWKDhse9hovQj-TrvbllP-0vLNQWEPLc3aUfVrWWR9i3NZZ-nxJZiIJPCF3xxIIyKaLh9a6Lh9J6Z-brsvVfbVJWXIGZhsu-uKk6Gwoqo56KqHdNaPF7lkPo5GAWfMrweCnJZ4o_j-oWm8BwTkXxrLib4XYKDO2JNqrNdbmy8rZ7UGgW_DVTiNyZi6LoRfSuvK45MWV2uzB_OJ9LBcqgscY4HyPvKrhGG4Peh4iXuBUCyQQ2IudM5GbeeMOAF3dnEzZff68SwE1H56CO6PtKhVQ6cFJMf7LwI5LFFio0qJnEDx-MejvU7PxmYW7R3MEbgjbsuEFU5KnRVYsgug3_Bq1vXdmP2qhebufFZwz26SwaFqyn3xjwCP8-GR7lWCZ2EvUvWtfxJ5_zgkZg06UsF4Eo_CWKFdp0ao43nemNJxOlMzFa6tPuCgplmD0oYoQ316f-bWK02-eJk56S7G4bZSk8cQfExfIZMjW2f-qrxvfxEpFiXsZF80BQwgRUOeKjsqidg2ihdldRkXGn3vX8p15mf1UgstU8y3DNd2_qJe1f_pEl6rWNXoxFdSRCTG7wTAqbCCmuDgCKGhNQY9tfJNsFqgWBIGkqKy88DN_HiWywJjJ-5u9aoe68yDK-E0TMDqs7ZrTely1wvmkl2yF0XQttaB30taxkIcRR-n0PRO-CRNA_9nJkw9ZsBb8oBjyqWH_mwSijT5g==\",\"summary\":[]},{\"id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":31,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":20,\"output_tokens_details\":{\"reasoning_tokens\":12},\"total_tokens\":51},\"user\":null,\"metadata\":{}},\"sequence_number\":11}\n\n" } }, { @@ -44,14 +44,14 @@ "headers": { "content-type": "application/json" }, - "body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Think briefly, then reply exactly with: Hello!\"}]},{\"type\":\"reasoning\",\"summary\":[],\"encrypted_content\":\"gAAAAABqYmP8aJHmIOttZSqCfZrRqPkxHEGRKL8agRHsomAtpHOM91zNm-YwW-Tupi5CFHy3SFlIatT1_VVZLSR50Mf1jZezXHUkIdB003BGPUwaeHlhlq3B63dukCmAGJso8xJvDw0YqYppy2FXNIoPoys3wI4n8LSTiUOm6yXEyUkaBt4sXLeWsg_NLqYo-ilmv60ppKV1vPQNZkHAX2N0PCZebSmzto5a896xV7jkKVsEMhanPnKP8N3G_ag_-B4ZPr01IAma4xJ2jXysRX_P-zN7AqGDRxBuuoIzylLizo68wNH9cXmnvQcCEkjKxOTpmQGCl0LxSnK_UnaLwErtj9FUMXDaDqbfugCZrCy5pUq0RgBSoAnZvOqH6pWpXPvclQI9Wsg-Ra3bv8sYtf2qYBMebaNIbI_BVVKN8Lrpj7UWODiRZImgfUWHuflaf98H1oD09f7e55gg7GlQ0r6EVZwbuX3GaEwsHbDe7KdsPmwV_RV_ZQpxm14aBypGtapXEAd8SMJxygZtuQKCTJuOwsgZug_I1cGw4EF5AIg5EWzVhvJmlzPrBqas-VdL0_VIfohJAm9181eYKX5ITeUZkdrNpwk7yMCLG4yHCOiP_x33Gyz3xOOUzNSVMy56F7f_K1Bd-eqHK4zMsYrS4-c2cbC5HPPNUUFh8-CaMaJ-34P2TvTHrxR9QY6PXMyfiEMhNC8Mpblhh3ShCvNWTAsrujWHmtDRp47cSNsiNxTSRMU75OsDviGcnuNCZDHBA-92N9g-nPqBvqJfFJXsHbpNzLRCgkv3O8ZOvMPOLLM4OZu1fELgAdKqWNupZgys6n1pUVsRGctECeVZ4NOafuvHvm6wUHRXuLLUN6ATJJ4RdSD_DAtnGD1QFstXwEeBHgBwMMppaIgBvZGbz5skrDm5NBv7AEdCl6UzZ-0zWP6AFxowTzTNuHUyIO-Buwvq_hmLn2hemhr0wOBBgTqFcW51cnybHt7sQD6AhGYlLjNnGu5XH7qcdTY=\"},{\"role\":\"assistant\",\"content\":\"Hello!\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Now reply exactly with: Done.\"}]}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"low\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":40,\"stream\":true}" + "body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Think briefly, then reply exactly with: Hello!\"}]},{\"type\":\"reasoning\",\"summary\":[],\"encrypted_content\":\"gAAAAABqEjXoGMCw3WDXpoD9151PEr2Lt8raW7KBKefQhZJGWx5f8jy152bApO6oE-Mr1BhUtfZNq3OPBVfSL4ioQ9bHREfujIBXgk9LUDBAz2Sle7KjOr9HaUV16A4HBiaFIRFjsHPS9G8yEySp1m6F1CD_WR6apyUGgugRh_y39EcOJmxPOzmiac5DVM6fraA1VpcGbqrZ1x2ANHFDOfnYTycPtPNTgzE7LjkYjDDWbT03uN1YxfP4pqjDVRzY14pA8bSZ8ys-pDv5kUFCAsw-OlU4jYKUXp-M8_6KTaRQP71LPwppt__zG_NJPfy-qUil4pOU8_NoxtxerHgLLXbfExZdzfpoGinoEjn7nj7BJDEtl-LNeNEb5c-1ZymNfVMp-Cs3fLEPkAV8rtHFtZ0MhE_07GKbGo7hTrOmkM4DydxmHsdWGNbXAG35cprslEA5P7p3GHFKnRs5hGs2eq-XcZ3yki64ZBOU_Tv6UR7nUH09gF1rdrJo3dpre6M00COwwdZ02zUP5KxCuI8FKu2jsZu9zgMVXDALsdtM5orTCVLXsn4rddWd111zE-vMjNmMMmktW2cHMjH7j1ooA-9P083koNVYiLi4UhMA64gTqgyl8MxkZekl7eFSMa7qk295NaHOKtFxzYYcZ9jdioCwSPSZ0ZZWLoNgrK7SWfRh0uaTHNcMZ3wq8ae6CguktIeVTCPTQAqJLQqd7AU0oOCKCJ7BWnC-L8UC6m7Pm9ZS958uUVeWBhgKHzMAGq9UeQB7IEeAcbMn3EDgOSfd8qCb8iwU9iG9dcu9axQwWU7pd7kd-T-He61W7z5wWgpx1KehWCxrN6kuKSo6p-uUfwVnJukreOn8BJNAzADQgz68bhmN9VGih7YcKVnLgwDwKditrjSd6-tfE0Baarj3jWENvT6ohY17R9FDrKS-2v8IIX6tGjoKJw8SRhaWLNv4vWlmxRgR0gdac3qumd0GKqsWSveNz01naA==\"},{\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello!\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Now reply exactly with: Done.\"}]}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"low\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":40,\"stream\":true}" }, "response": { "status": 200, "headers": { "content-type": "text/event-stream; charset=utf-8" }, - "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_06418d4b810c5ed5016a6263fc610081909e978e5ca1a9edd0\",\"object\":\"response\",\"created_at\":1784833020,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_06418d4b810c5ed5016a6263fc610081909e978e5ca1a9edd0\",\"object\":\"response\",\"created_at\":1784833020,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_06418d4b810c5ed5016a6263fd13888190829edf0774b5b6e1\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_06418d4b810c5ed5016a6263fd13888190829edf0774b5b6e1\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"Done\",\"item_id\":\"msg_06418d4b810c5ed5016a6263fd13888190829edf0774b5b6e1\",\"logprobs\":[],\"obfuscation\":\"M3ODGG2egQCH\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".\",\"item_id\":\"msg_06418d4b810c5ed5016a6263fd13888190829edf0774b5b6e1\",\"logprobs\":[],\"obfuscation\":\"fm15a3vpfYAW4br\",\"output_index\":0,\"sequence_number\":5}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_06418d4b810c5ed5016a6263fd13888190829edf0774b5b6e1\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":6,\"text\":\"Done.\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_06418d4b810c5ed5016a6263fd13888190829edf0774b5b6e1\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Done.\"},\"sequence_number\":7}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_06418d4b810c5ed5016a6263fd13888190829edf0774b5b6e1\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Done.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":8}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_06418d4b810c5ed5016a6263fc610081909e978e5ca1a9edd0\",\"object\":\"response\",\"created_at\":1784833020,\"status\":\"completed\",\"background\":false,\"completed_at\":1784833021,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[{\"id\":\"msg_06418d4b810c5ed5016a6263fd13888190829edf0774b5b6e1\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Done.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"mode\":\"standard\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":35,\"input_tokens_details\":{\"cache_write_tokens\":0,\"cached_tokens\":0},\"output_tokens\":6,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":41},\"user\":null,\"metadata\":{}},\"sequence_number\":9}\n\n" + "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e991c88195a4d2f9766babd985\",\"object\":\"response\",\"created_at\":1779578345,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e991c88195a4d2f9766babd985\",\"object\":\"response\",\"created_at\":1779578345,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"rs_0a0794dab3b8ec7d016a1235ea4dd88195a32179255ed6c532\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXqB-kOX_0QAeoEksgNjwSbtGmVEQuMj5ODcFV6b7Kp3E8RoHRRmSXtRH0rtNbZRbhKz5jM48DUpDI1WTeO2HqCd_A3fsSgFxp5ACGVFjPWjfvP2JMdDkpoOo5gu2zy7WsWY0fseocQQ5q_jfG6SWw0fyaeeqfdQ9HkcHyg6gVEl5skb4L8_2lD5nClmLlNVVh5JCuXRH9eYysrfO19NOZ29A2MVUX-XgB6mmK5uSb1jE43GhrEPPYrMbB5JyzM6B-yeB8rE4H2wx530hQqtxwSZREa8G03rzTJ49_KAPWl0djGDDtufUX-t4EpBHo6loA3PMuiZ3VsJTkkPpEqkm6QQyAVkQ_8AdRu12CqHbFdu73I-BnArzr33yW6reNUjnZjFV5bWDyxIMh6ljy3O_2nGk-qdTLt6bGJbEjTdPj1hi7icYZTVPqofPU4pjlo9BnIBheo-4u9pA26V9G9vDAtM4myDdMEe4pnieUztBUYPUOVMaG2U9gqtNs6iPehKo3BeKy4lhYPorL2OPmf1lVUQOCW1MBbwT5xt1kjOVw7LggnyjrBsXVvDBWg0AFcvm14r3ZQezPgLetQfSx56mVEJpui9BuVSUg2Xvqb5tCCip6TipUVvzZJKKkxN43o8N6UVXLIn6wgstAn9727JgBEsjMxzvuOWaaI-qM3dWMcFzFSGvKb6gTiF37AhSOzosf31hnsGx8AnGmLmbuW3IMhZXZZMHgVUHx4p8pNRPeoCE-Bv833KZbRfVxe6tbmkLadBjXYaCQqPXDHBR7qbpfu4_M9UAy0kpic-joepxQKsCT2t6vsNaThRYaN3PtIgjs5xxAfa9yKeIYak8yiP96CUlME9Y7zaOIydPWYBhWLf3phQsWMax9eKdLDh19f0Y0iAJPpk--1xd2OVTuDxKIMpA==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"rs_0a0794dab3b8ec7d016a1235ea4dd88195a32179255ed6c532\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXqW5MInCBPKulZ1lizyFtOaKUpKgHldAXVjTTs4XFE45gtxC1NbJoOi2tHoQhpfq-JGxtjSQEDTHnMCiLLhyqvQ4GlWVF4n51xcFVC_WgymkZqDxG6xPw8ITAsRI5vb8HiPO6EmmKt6xGIVXOjrrxRNAY3xtrByeYSvnCa6FDUHEkMeXmwllBalCeQPNDPl0Ub2ehuchNG0loMVLJoOjT-2KgDrXlOa6rCn3nUf4U1W5JA_kHytlgrD0IPbs7nY8wemdynJRXBoNSOT_U3nQSB6j-i4KIJAdLiUs9LVWMYleqmFQNs8S4dC3i5DfpHXWUMZ5Ai1d3gbvMP8bH7fsUyfIhyiDUvlgr6PZ9rfh8JqkjOpiQ7NFtSDuHQGdx__W3qi23WPDp3iQjKxVl1oUXfbMzsPE4bmNN9dnJ9qTQ43kvw8GyrGrSqRS8jCKuk9bxqeR_ibj4KoDdxvVbUeGMg3WKANfCRsNXlxwYtMpu3I4HxKm5EuMNKDg_e9RFH2wFEDm9wCKMZrC_5LShgKhSfhsk3yJ47Mit0zYdX27kyHGlZvpzLPYKdtHW1O15KNT4gFKBrIguCtjXz2Lb42ENM6Jo8BTY7BZbf0hXZ4A5mFNl_gyLVHWEHpR1GcYiJbs9RtQ-7qX_PTeZ11iFY3a7_jM7TK3WEN2IuG0OKbZVHvOkVcvyBEgIbzSzCzhtC-j584knI4WmYiqnltuwRcR2N3sxYY3vMcYGA2_AU5kYlZcJcztapTTW-aKbyGxPcw5D_dqb5mGpDqJgquye-qOufDt4Fd7cSc-g8awqR8QPsLz-ZDPLMB9JKQ3VqLQlNCKDUoDodGOAL3h-7EQG66osALfhpdsWcNmuVqlb0lNAXklrsZJtRKBU4pJ1UGCyVDwde7nv6I9PW19VumaRrJhc2cC52qUoyihvUo8xJsElaFp7-EHn5ymS4znZhRfyA_4UDL4rwj-3DqbMwNeDJrgBc3w==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":3}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":4}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"Done\",\"item_id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"logprobs\":[],\"obfuscation\":\"7tyE7hMvNOTM\",\"output_index\":1,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".\",\"item_id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"logprobs\":[],\"obfuscation\":\"RGXvuTTSJS3AT5E\",\"output_index\":1,\"sequence_number\":7}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"logprobs\":[],\"output_index\":1,\"sequence_number\":8,\"text\":\"Done.\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Done.\"},\"sequence_number\":9}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Done.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":10}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e991c88195a4d2f9766babd985\",\"object\":\"response\",\"created_at\":1779578345,\"status\":\"completed\",\"background\":false,\"completed_at\":1779578346,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[{\"id\":\"rs_0a0794dab3b8ec7d016a1235ea4dd88195a32179255ed6c532\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXq8oliF2VeqiOUi-jUdi49emjffD6wtbmxlwQWbJ6tSxXIjyXvCeclOqKx83G83GyDOJqvR4L_D8V_ebJtgG87ahWB8Rr9LEQDoLT24n4Vz279xtHMxEGgv7f0NmaXu2dGFeFY_s2RhH-DqNE7V4nEkS7odJOTkhxTKgEcxtz3dDlEnGU7IgN2sD1lh9y90BD3ysvARegy4Cs0DhUjLOvkx11G9lk5dQ3yo1ek8JhTHpnVSYrLDYIudCh6pfu1yP1tx8xbxDHUcwlNclU9Hp_9ils5FhZNWC_tiLDscXXvRPBgMF77jdOicCV6cyUV0Snsu1_KSRbm4rLtgXLXVMqFyYpxdyicsD577e4yZ0VVXT4Oo_af0eDh3I3ZPIWui38EmYuoRhvQuYZkqjhGd_xOkvjQF4_Tp6cyNO0XdAMGMoYG-5npHC0gcPpv56qYGX8ffj0P8ZyR9shn3H7kcQqE2YXXBa42VKK0poPbC996xSqFNW7ygePel41h493XlJ70wnP50vFY5s0raNFf9eLP3YYmLxiPks9gshayGwUQXNNwrSimoQv3OeJzRzihbzZNWTfhR4xKs53nlXMjwnnXwHRH5D07vJg_1zU7BQzJ-QRLZnsnhIOq3psHt1yuoCtsSTKBN6HPiR81F-snIttJiUAiYsgv_ajwPxxnKP0FnFXQfBuaUAtAOD5G_3MC1yECjzq-YI4MDOXj4dsIGnHkdzXo-DV2lXMl2WnPqytoUkugp14SWbJso-eDsN5QivqspnYc1VsdNAaOOgjBiHmi-bACI1CykrkuiYJm1nOHAH4L4IQjpd0pcNm-Dk7z9LGIE5lwKI07hLXp_ByhVXRT8xWuugl43pzoM1jgYD4LjTjScC3ymauqqvKjjoHfnt0Zma0eVDeQrnVT6W9RQ9wDt5KVebrrwJTqlaNV0HywZJo3gwFy-Qq5MfwAwwC-GdjMsER1TgXO_E5kFZZD4sNVgw==\",\"summary\":[]},{\"id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Done.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":35,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":20,\"output_tokens_details\":{\"reasoning_tokens\":12},\"total_tokens\":55},\"user\":null,\"metadata\":{}},\"sequence_number\":11}\n\n" } } ] diff --git a/packages/llm/test/provider/openai-responses-phase.recorded.test.ts b/packages/llm/test/provider/openai-responses-phase.recorded.test.ts deleted file mode 100644 index f8ea260541..0000000000 --- a/packages/llm/test/provider/openai-responses-phase.recorded.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { describe, expect } from "bun:test" -import { Effect } from "effect" -import { LLM, Message } from "../../src" -import * as OpenAI from "../../src/providers/openai" -import { OpenAIResponses } from "../../src/protocols/openai-responses" -import { LLMClient } from "../../src/route" -import { weatherTool } from "../recorded-scenarios" -import { recordedTests } from "../recorded-test" - -const model = OpenAI.configure({ - apiKey: process.env.OPENAI_API_KEY ?? "fixture", -}).responses("gpt-5.6-sol") - -const recorded = recordedTests({ - prefix: "openai-responses-phase", - provider: "openai", - protocol: "openai-responses", - requires: ["OPENAI_API_KEY"], -}) - -describe("OpenAI Responses phase recorded", () => { - recorded.effect.with("round-trips commentary into a final answer", { tags: ["phase", "tool"] }, () => - Effect.gen(function* () { - const user = Message.user("What is the weather in Paris?") - const first = yield* LLMClient.generate( - LLM.request({ - model, - system: - "Before calling get_weather, briefly tell the user you are checking. Then call get_weather exactly once. Do not provide the final answer until its result is available.", - messages: [user], - tools: [weatherTool], - generation: { maxTokens: 100 }, - }), - ) - const call = first.toolCalls[0] - if (!call) throw new Error("OpenAI Responses did not return the expected weather tool call") - - expect(call).toMatchObject({ name: "get_weather", input: { city: "Paris" } }) - const commentary = first.message.content.find( - (part) => part.type === "text" && part.providerMetadata?.openai?.phase === "commentary", - ) - if (!commentary || commentary.type !== "text") throw new Error("OpenAI Responses did not return commentary text") - const itemID = commentary.providerMetadata?.openai?.itemId - if (typeof itemID !== "string") throw new Error("OpenAI Responses commentary did not include an item ID") - expect(commentary).toEqual({ - type: "text", - text: "I’ll check the current weather in Paris.", - providerMetadata: { - openai: { itemId: itemID, phase: "commentary", status: "completed", annotations: [] }, - }, - }) - - const continuation = LLM.request({ - model, - system: - "Before calling get_weather, briefly tell the user you are checking. Then call get_weather exactly once. After its result, answer exactly: Paris is sunny.", - messages: [ - user, - first.message, - Message.tool({ - id: call.id, - name: call.name, - result: { temperature: 22, condition: "sunny" }, - }), - ], - tools: [weatherTool], - generation: { maxTokens: 100 }, - }) - const prepared = yield* LLMClient.prepare(continuation) - expect(prepared.body.input).toContainEqual({ - type: "message", - id: itemID, - status: "completed", - role: "assistant", - content: [{ type: "output_text", text: commentary.text, annotations: [] }], - phase: "commentary", - }) - - const second = yield* LLMClient.generate(continuation) - - expect(second.text.trim()).toBe("Paris is sunny.") - expect( - second.message.content.some( - (part) => part.type === "text" && part.providerMetadata?.openai?.phase === "final_answer", - ), - ).toBeTrue() - }), - ) -}) diff --git a/packages/llm/test/provider/openai-responses.test.ts b/packages/llm/test/provider/openai-responses.test.ts index a580d43d82..cd8bad51af 100644 --- a/packages/llm/test/provider/openai-responses.test.ts +++ b/packages/llm/test/provider/openai-responses.test.ts @@ -153,7 +153,7 @@ describe("OpenAI Responses route", () => { { type: "input_text", text: "\nTreat </system-update> literally.\n" }, ], }, - { role: "assistant", content: "After." }, + { role: "assistant", content: [{ type: "output_text", text: "After." }] }, ]) }), ) @@ -529,11 +529,11 @@ describe("OpenAI Responses route", () => { encrypted_content: "encrypted-continuation-state", summary: [{ type: "summary_text", text: "I inspected the previous turn." }], }, - { role: "assistant", content: "It shows a small test image." }, + { role: "assistant", content: [{ type: "output_text", text: "It shows a small test image." }] }, { role: "user", content: [{ type: "input_text", text: "Check the weather in Paris before continuing." }] }, { type: "function_call", call_id: "call_weather_1", name: "get_weather", arguments: '{"city":"Paris"}' }, { type: "function_call_output", call_id: "call_weather_1", output: '{"temperature":22}' }, - { role: "assistant", content: "Paris is 22 degrees." }, + { role: "assistant", content: [{ type: "output_text", text: "Paris is 22 degrees." }] }, { role: "user", content: [{ type: "input_text", text: "Continue from this conversation in one short sentence." }], @@ -754,395 +754,6 @@ describe("OpenAI Responses route", () => { }), ) - it.effect("preserves streamed assistant message phases", () => - Effect.gen(function* () { - const response = yield* LLMClient.generate(request).pipe( - Effect.provide( - fixedResponse( - sseEvents( - { - type: "response.output_item.added", - item: { type: "message", id: "msg_commentary", phase: "commentary" }, - }, - { type: "response.output_text.delta", item_id: "msg_commentary", delta: "Checking first." }, - { type: "response.output_text.done", item_id: "msg_commentary" }, - { - type: "response.output_item.done", - item: { type: "message", id: "msg_commentary", phase: "commentary" }, - }, - { - type: "response.output_item.added", - item: { type: "message", id: "msg_final", phase: "final_answer" }, - }, - { type: "response.output_text.delta", item_id: "msg_final", delta: "Finished." }, - { type: "response.output_text.done", item_id: "msg_final" }, - { - type: "response.output_item.done", - item: { type: "message", id: "msg_final", phase: "final_answer" }, - }, - { type: "response.completed", response: { id: "resp_1" } }, - ), - ), - ), - ) - - expect(response.events.filter((event) => event.type.startsWith("text-"))).toEqual([ - { - type: "text-start", - id: "msg_commentary", - providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, - }, - { type: "text-delta", id: "msg_commentary", text: "Checking first." }, - { - type: "text-end", - id: "msg_commentary", - providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, - }, - { - type: "text-start", - id: "msg_final", - providerMetadata: { openai: { itemId: "msg_final", phase: "final_answer" } }, - }, - { type: "text-delta", id: "msg_final", text: "Finished." }, - { - type: "text-end", - id: "msg_final", - providerMetadata: { openai: { itemId: "msg_final", phase: "final_answer" } }, - }, - ]) - expect(response.message.content).toEqual([ - { - type: "text", - text: "Checking first.", - providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, - }, - { - type: "text", - text: "Finished.", - providerMetadata: { openai: { itemId: "msg_final", phase: "final_answer" } }, - }, - ]) - }), - ) - - it.effect("preserves phased message and content boundaries", () => - Effect.gen(function* () { - const response = yield* LLMClient.generate(request).pipe( - Effect.provide( - fixedResponse( - sseEvents( - { - type: "response.output_item.added", - item: { type: "message", id: "msg_commentary", phase: "commentary" }, - }, - { - type: "response.output_text.delta", - item_id: "msg_commentary", - content_index: 0, - delta: "First.", - }, - { - type: "response.output_item.added", - item: { type: "message", id: "msg_commentary" }, - }, - { - type: "response.output_text.done", - item_id: "msg_commentary", - content_index: 0, - text: "First.", - }, - { - type: "response.output_text.done", - item_id: "msg_commentary", - content_index: 1, - text: "Second.", - }, - { - type: "response.output_item.done", - item: { type: "message", id: "msg_commentary" }, - }, - { - type: "response.output_item.added", - item: { type: "message", id: "msg_commentary_2", phase: "commentary" }, - }, - { - type: "response.output_text.delta", - item_id: "msg_commentary_2", - content_index: 0, - delta: "Thi", - }, - { - type: "response.output_text.done", - item_id: "msg_commentary_2", - content_index: 0, - text: "Third.", - }, - { - type: "response.output_item.done", - item: { type: "message", id: "msg_commentary_2", phase: "commentary" }, - }, - { - type: "response.output_item.added", - item: { type: "message", id: "openai-text-0" }, - }, - { - type: "response.output_text.done", - item_id: "openai-text-0", - content_index: 0, - text: "Final.", - }, - { - type: "response.output_item.done", - item: { - type: "message", - id: "openai-text-0", - phase: "final_answer", - content: [ - { - type: "output_text", - text: "Final.", - annotations: [ - { - type: "url_citation", - url: "https://example.com", - title: "Example", - start_index: 0, - end_index: 6, - }, - ], - }, - ], - }, - }, - { - type: "response.output_item.added", - item: { type: "message", id: "msg_null", phase: null }, - }, - { - type: "response.output_text.done", - item_id: "msg_null", - content_index: 0, - text: "Nullable.", - }, - { - type: "response.output_item.done", - item: { type: "message", id: "msg_null", phase: null }, - }, - { - type: "response.output_item.added", - item: { type: "message", id: "msg_unphased" }, - }, - { - type: "response.output_text.done", - item_id: "msg_unphased", - content_index: 0, - text: "Unphased.", - }, - { - type: "response.output_item.done", - item: { type: "message", id: "msg_unphased" }, - }, - { type: "response.completed", response: { id: "resp_1" } }, - ), - ), - ), - ) - - expect(response.message.content).toEqual([ - { - type: "text", - text: "First.", - providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, - }, - { - type: "text", - text: "Second.", - providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, - }, - { - type: "text", - text: "Third.", - providerMetadata: { openai: { itemId: "msg_commentary_2", phase: "commentary" } }, - }, - { - type: "text", - text: "Final.", - providerMetadata: { - openai: { - itemId: "openai-text-0", - phase: "final_answer", - annotations: [ - { - type: "url_citation", - url: "https://example.com", - title: "Example", - start_index: 0, - end_index: 6, - }, - ], - }, - }, - }, - { - type: "text", - text: "Nullable.", - providerMetadata: { openai: { itemId: "msg_null", phase: null } }, - }, - { - type: "text", - text: "Unphased.", - providerMetadata: { openai: { itemId: "msg_unphased" } }, - }, - ]) - - expect(response.events.filter((event) => event.type.startsWith("text-"))).toEqual([ - { - type: "text-start", - id: "msg_commentary", - providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, - }, - { type: "text-delta", id: "msg_commentary", text: "First." }, - { - type: "text-end", - id: "msg_commentary", - providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, - }, - { - type: "text-start", - id: "openai-text-0", - providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, - }, - { type: "text-delta", id: "openai-text-0", text: "Second." }, - { - type: "text-end", - id: "openai-text-0", - providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, - }, - { - type: "text-start", - id: "msg_commentary_2", - providerMetadata: { openai: { itemId: "msg_commentary_2", phase: "commentary" } }, - }, - { type: "text-delta", id: "msg_commentary_2", text: "Thi" }, - { type: "text-delta", id: "msg_commentary_2", text: "rd." }, - { - type: "text-end", - id: "msg_commentary_2", - providerMetadata: { openai: { itemId: "msg_commentary_2", phase: "commentary" } }, - }, - { - type: "text-start", - id: "openai-text-1", - providerMetadata: { openai: { itemId: "openai-text-0" } }, - }, - { type: "text-delta", id: "openai-text-1", text: "Final." }, - { - type: "text-end", - id: "openai-text-1", - providerMetadata: { - openai: { - itemId: "openai-text-0", - phase: "final_answer", - annotations: [ - { - type: "url_citation", - url: "https://example.com", - title: "Example", - start_index: 0, - end_index: 6, - }, - ], - }, - }, - }, - { - type: "text-start", - id: "msg_null", - providerMetadata: { openai: { itemId: "msg_null", phase: null } }, - }, - { type: "text-delta", id: "msg_null", text: "Nullable." }, - { - type: "text-end", - id: "msg_null", - providerMetadata: { openai: { itemId: "msg_null", phase: null } }, - }, - { - type: "text-start", - id: "msg_unphased", - providerMetadata: { openai: { itemId: "msg_unphased" } }, - }, - { type: "text-delta", id: "msg_unphased", text: "Unphased." }, - { - type: "text-end", - id: "msg_unphased", - providerMetadata: { openai: { itemId: "msg_unphased" } }, - }, - ]) - - const prepared = yield* LLMClient.prepare( - LLM.request({ model, messages: [response.message] }), - ) - expect(prepared.body.input).toEqual([ - { - type: "message", - id: "msg_commentary", - status: "completed", - role: "assistant", - phase: "commentary", - content: [ - { type: "output_text", text: "First.", annotations: [] }, - { type: "output_text", text: "Second.", annotations: [] }, - ], - }, - { - type: "message", - id: "msg_commentary_2", - status: "completed", - role: "assistant", - phase: "commentary", - content: [{ type: "output_text", text: "Third.", annotations: [] }], - }, - { - type: "message", - id: "openai-text-0", - status: "completed", - role: "assistant", - phase: "final_answer", - content: [ - { - type: "output_text", - text: "Final.", - annotations: [ - { - type: "url_citation", - url: "https://example.com", - title: "Example", - start_index: 0, - end_index: 6, - }, - ], - }, - ], - }, - { - type: "message", - id: "msg_null", - status: "completed", - role: "assistant", - phase: null, - content: [{ type: "output_text", text: "Nullable.", annotations: [] }], - }, - { - type: "message", - id: "msg_unphased", - status: "completed", - role: "assistant", - content: [{ type: "output_text", text: "Unphased.", annotations: [] }], - }, - ]) - }), - ) - it.effect("parses reasoning summary stream fixtures", () => Effect.gen(function* () { const body = sseEvents( @@ -1336,7 +947,7 @@ describe("OpenAI Responses route", () => { encrypted_content: "encrypted-state", summary: [{ type: "summary_text", text: "Checked the previous diff." }], }, - { role: "assistant", content: "The parser changed." }, + { role: "assistant", content: [{ type: "output_text", text: "The parser changed." }] }, { role: "user", content: [{ type: "input_text", text: "Summarize it." }] }, ], }) @@ -1384,69 +995,13 @@ describe("OpenAI Responses route", () => { ) expect(prepared.body.input).toEqual([ - { role: "assistant", content: "Before." }, + { role: "assistant", content: [{ type: "output_text", text: "Before." }] }, { type: "reasoning", encrypted_content: "encrypted-state", summary: [{ type: "summary_text", text: "Checked order." }], }, - { role: "assistant", content: "After." }, - ]) - }), - ) - - it.effect("round-trips assistant message phases", () => - Effect.gen(function* () { - const prepared = yield* LLMClient.prepare( - LLM.request({ - model, - messages: [ - Message.assistant([ - { - type: "text", - text: "Checking first.", - providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } }, - }, - { - type: "text", - text: "Still checking.", - providerMetadata: { openai: { itemId: "msg_commentary_2", phase: "commentary" } }, - }, - { - type: "text", - text: "Finished.", - providerMetadata: { openai: { itemId: "msg_final", phase: "final_answer" } }, - }, - ]), - ], - }), - ) - - expect(prepared.body.input).toEqual([ - { - type: "message", - id: "msg_commentary", - status: "completed", - role: "assistant", - phase: "commentary", - content: [{ type: "output_text", text: "Checking first.", annotations: [] }], - }, - { - type: "message", - id: "msg_commentary_2", - status: "completed", - role: "assistant", - phase: "commentary", - content: [{ type: "output_text", text: "Still checking.", annotations: [] }], - }, - { - type: "message", - id: "msg_final", - status: "completed", - role: "assistant", - phase: "final_answer", - content: [{ type: "output_text", text: "Finished.", annotations: [] }], - }, + { role: "assistant", content: [{ type: "output_text", text: "After." }] }, ]) }), ) @@ -1565,13 +1120,7 @@ describe("OpenAI Responses route", () => { }, }, }, - { - type: "text", - text: "The parser changed.", - providerMetadata: { - openai: { itemId: "msg_1", phase: "final_answer", status: "completed" }, - }, - }, + { type: "text", text: "The parser changed." }, ]), Message.user("Summarize it."), ], @@ -1582,7 +1131,7 @@ describe("OpenAI Responses route", () => { expect(prepared.body).toMatchObject({ input: [ { role: "user", content: [{ type: "input_text", text: "What changed?" }] }, - { role: "assistant", content: "The parser changed.", phase: "final_answer" }, + { role: "assistant", content: [{ type: "output_text", text: "The parser changed." }] }, { role: "user", content: [{ type: "input_text", text: "Summarize it." }] }, ], store: false, diff --git a/packages/opencode/test/session/llm-native.test.ts b/packages/opencode/test/session/llm-native.test.ts index 09b2ffbd16..dd4d9cc174 100644 --- a/packages/opencode/test/session/llm-native.test.ts +++ b/packages/opencode/test/session/llm-native.test.ts @@ -119,7 +119,7 @@ const storedSession = { const openAIResponses = { user: (text: string) => ({ role: "user", content: [{ type: "input_text", text }] }), - assistant: (text: string) => ({ role: "assistant", content: text }), + assistant: (text: string) => ({ role: "assistant", content: [{ type: "output_text", text }] }), openaiReasoning: (text: string, encryptedContent: string) => ({ type: "reasoning", encrypted_content: encryptedContent, From 2b2aacc93975330f9fd045d4306f698b0c6a8f8f Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:44:00 -0500 Subject: [PATCH 004/129] fix(provider): generalize Claude adaptive thinking (#38757) --- packages/opencode/src/provider/transform.ts | 53 ++++++--- .../opencode/test/provider/transform.test.ts | 112 +++++++++++++++++- 2 files changed, 145 insertions(+), 20 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 81759160bf..705af3d5c1 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -635,24 +635,23 @@ function openaiCompatibleReasoningEfforts(id: string) { return gpt5CodexReasoningEfforts(apiId) ?? versionedGpt5ReasoningEfforts(apiId) ?? OPENAI_EFFORTS } -function anthropicOpus47OrLater(apiId: string) { - // Matches "opus-4.7" (Anthropic/Bedrock/Vertex) and "claude-4.7-opus" (SAP AI Core inverted). - // Greedy \d+ correctly extends to multi-digit majors (e.g. "claude-10.0-opus") for forward compatibility. - const version = /opus-(\d+)[.-](\d+)(?:[.@-]|$)|claude-(\d+)[.-](\d+)-opus(?:[.@-]|$)/i.exec(apiId) - if (!version) return false - const major = Number(version[1] ?? version[3]) - const minor = Number(version[2] ?? version[4]) +function anthropicUsesModernAdaptiveThinking(apiId: string) { + if (!apiId.toLowerCase().includes("claude-")) return false + // Covers family-first IDs such as claude-opus-4.7 and version-first IDs such as claude-4.7-opus. + // Limit minors to two digits so release dates in IDs such as claude-opus-4-20250514 are not versions. + const version = /claude-(?:[a-z]+-)?(\d+)(?:[.-](\d{1,2}))?(?:[.@-]|$)/i.exec(apiId) + if (!version) return true + const major = Number(version[1]) + const minor = Number(version[2] ?? 0) return major > 4 || (major === 4 && minor >= 7) } -function anthropicSonnet5OrLater(apiId: string) { - const version = /sonnet-(\d+)(?:[.@-]|$)|claude-(\d+)-sonnet(?:[.@-]|$)/i.exec(apiId) - if (!version) return false - return Number(version[1] ?? version[2]) >= 5 +function anthropicOpus45(apiId: string) { + return ["opus-4-5", "opus-4.5"].some((value) => apiId.includes(value)) } function anthropicAdaptiveEfforts(apiId: string): string[] | null { - if (anthropicOpus47OrLater(apiId) || anthropicSonnet5OrLater(apiId) || apiId.includes("fable-5")) { + if (anthropicUsesModernAdaptiveThinking(apiId)) { return ["low", "medium", "high", "xhigh", "max"] } if ( @@ -666,7 +665,7 @@ function anthropicAdaptiveEfforts(apiId: string): string[] | null { } function anthropicOmitsThinking(apiId: string) { - return anthropicOpus47OrLater(apiId) || anthropicSonnet5OrLater(apiId) || apiId.includes("fable-5") + return anthropicUsesModernAdaptiveThinking(apiId) } function googleThinkingLevelEfforts(apiId: string) { @@ -990,8 +989,10 @@ export function variants(model: Provider.Model): Record model.api.id.includes(v))) { - return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { effort }])) + if (anthropicOpus45(model.api.id)) { + return Object.fromEntries( + WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, anthropicOpus45Effort(model, effort)]), + ) } return { @@ -1103,7 +1104,7 @@ export function variants(model: Provider.Model): Record [ @@ -1711,6 +1712,14 @@ function reasoningEffort(model: Provider.Model, effort: string) { ...(anthropicOmitsThinking(model.api.id) ? { display: "summarized" } : {}), }, } + if (anthropicOpus45(model.api.id)) + return { + reasoningConfig: { + type: "enabled", + budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)), + maxReasoningEffort: effort, + }, + } if (model.api.id.includes("anthropic")) return return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } } case "@ai-sdk/gateway": @@ -1751,7 +1760,7 @@ function reasoningEffort(model: Provider.Model, effort: string) { } function anthropicEffort(model: Provider.Model, effort: string) { - if (["opus-4-5", "opus-4.5"].some((value) => model.api.id.includes(value))) return { effort } + if (anthropicOpus45(model.api.id)) return anthropicOpus45Effort(model, effort) // Kimi defaults to omitting adaptive thinking text unless summarized display is requested. if (isKimiFamily(model)) return { thinking: { type: "adaptive", display: "summarized" }, effort } if (!anthropicAdaptiveEfforts(model.api.id)) return @@ -1764,6 +1773,16 @@ function anthropicEffort(model: Provider.Model, effort: string) { } } +function anthropicOpus45Effort(model: Provider.Model, effort: string) { + return { + thinking: { + type: "enabled", + budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)), + }, + effort, + } +} + function reasoningBudget(model: Provider.Model, budget: number) { switch (model.api.npm) { case "@openrouter/ai-sdk-provider": diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index ef2b275035..93e166c4a8 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3213,6 +3213,7 @@ describe("ProviderTransform.reasoningVariants", () => { { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, "claude-opus-4-7", ], + ["@ai-sdk/anthropic", { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, "claude-opus-5"], ["@ai-sdk/google", { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } }], ["@ai-sdk/google-vertex", { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } }], [ @@ -3264,13 +3265,18 @@ describe("ProviderTransform.reasoningVariants", () => { ) }) - test("uses bare effort for Claude Opus 4.5", () => { + test("combines effort with extended thinking for Claude Opus 4.5", () => { expect( ProviderTransform.reasoningVariants( model([{ type: "effort", values: ["high"] }]), target("@ai-sdk/anthropic", "claude-opus-4-5"), ), - ).toEqual({ high: { effort: "high" } }) + ).toEqual({ + high: { + thinking: { type: "enabled", budgetTokens: 16_000 }, + effort: "high", + }, + }) }) test("uses explicit effort metadata for Anthropic-compatible models", () => { @@ -3319,6 +3325,38 @@ describe("ProviderTransform.reasoningVariants", () => { }) }) + test("uses adaptive reasoning config for Claude Opus 5 on Bedrock", () => { + const result = ProviderTransform.reasoningVariants( + model([{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }]), + target("@ai-sdk/amazon-bedrock", "us.anthropic.claude-opus-5"), + ) + expect(Object.keys(result ?? {})).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(result?.high).toEqual({ + reasoningConfig: { + type: "adaptive", + maxReasoningEffort: "high", + display: "summarized", + }, + }) + }) + + test("combines effort with extended thinking for Claude Opus 4.5 on Bedrock", () => { + expect( + ProviderTransform.reasoningVariants( + model([{ type: "effort", values: ["high"] }]), + target("@ai-sdk/amazon-bedrock", "us.anthropic.claude-opus-4-5-20251101-v1:0"), + ), + ).toEqual({ + high: { + reasoningConfig: { + type: "enabled", + budgetTokens: 16_000, + maxReasoningEffort: "high", + }, + }, + }) + }) + test("does not replace unsupported Anthropic Bedrock effort options with token budgets", () => { expect( ProviderTransform.reasoningVariants( @@ -4617,11 +4655,17 @@ describe("ProviderTransform.variants", () => { describe("@ai-sdk/anthropic", () => { for (const testCase of [ + { + name: "opus 4 dated", + apiIds: ["claude-opus-4-20250514"], + efforts: ["high", "max"], + expectedHigh: { thinking: { type: "enabled", budgetTokens: 16000 } }, + }, { name: "opus 4.5", apiIds: ["claude-opus-4-5-20251101", "claude-opus-4.5-20251101"], efforts: ["low", "medium", "high"], - expectedHigh: { effort: "high" }, + expectedHigh: { thinking: { type: "enabled", budgetTokens: 16000 }, effort: "high" }, }, { name: "sonnet 4.6", @@ -4653,6 +4697,18 @@ describe("ProviderTransform.variants", () => { efforts: ["low", "medium", "high", "xhigh", "max"], expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, }, + { + name: "opus 5", + apiIds: ["claude-opus-5", "claude-opus-5-20260724"], + efforts: ["low", "medium", "high", "xhigh", "max"], + expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, + }, + { + name: "unversioned future model", + apiIds: ["claude-future"], + efforts: ["low", "medium", "high", "xhigh", "max"], + expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, + }, { name: "fable 5", apiIds: ["claude-fable-5"], @@ -4772,6 +4828,28 @@ describe("ProviderTransform.variants", () => { effort: "high", }) }) + + test("opus 5 uses adaptive reasoning for Vertex model IDs", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "google-vertex-anthropic/claude-opus-5@default", + providerID: "google-vertex-anthropic", + api: { + id: "claude-opus-5@default", + url: "https://us-central1-aiplatform.googleapis.com", + npm: "@ai-sdk/google-vertex/anthropic", + }, + }), + ) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(result.high).toEqual({ + thinking: { + type: "adaptive", + display: "summarized", + }, + effort: "high", + }) + }) }) describe("@ai-sdk/amazon-bedrock", () => { @@ -4867,6 +4945,28 @@ describe("ProviderTransform.variants", () => { }) }) + test("anthropic opus 5 returns adaptive reasoning options with xhigh", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "bedrock/anthropic-claude-opus-5", + providerID: "bedrock", + api: { + id: "us.anthropic.claude-opus-5-v1:0", + url: "https://bedrock.amazonaws.com", + npm: "@ai-sdk/amazon-bedrock", + }, + }), + ) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(result.high).toEqual({ + reasoningConfig: { + type: "adaptive", + maxReasoningEffort: "high", + display: "summarized", + }, + }) + }) + test("returns WIDELY_SUPPORTED_EFFORTS with reasoningConfig", () => { const model = createMockModel({ id: "bedrock/llama-4", @@ -5055,6 +5155,12 @@ describe("ProviderTransform.variants", () => { efforts: ["low", "medium", "high", "xhigh", "max"], thinking: { type: "adaptive", display: "summarized" }, }, + { + name: "opus 5", + apiIds: ["anthropic--claude-opus-5", "anthropic--claude-5-opus"], + efforts: ["low", "medium", "high", "xhigh", "max"], + thinking: { type: "adaptive", display: "summarized" }, + }, ]) { for (const apiId of testCase.apiIds) { test(`${testCase.name} ${apiId} returns adaptive thinking variants under modelParams`, () => { From a85d8d23aa297b3051e642c28e3fc79b457fc4bc Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 24 Jul 2026 22:18:13 +0000 Subject: [PATCH 005/129] sync release versions for v1.18.5 --- bun.lock | 56 ++++++++++----------- packages/app/package.json | 2 +- packages/cli/package.json | 2 +- packages/codemode/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/console/support/package.json | 2 +- packages/core/package.json | 2 +- packages/desktop/package.json | 2 +- packages/effect-drizzle-sqlite/package.json | 2 +- packages/effect-sqlite-node/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/function/package.json | 2 +- packages/http-recorder/package.json | 2 +- packages/llm/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/server/package.json | 2 +- packages/session-ui/package.json | 2 +- packages/slack/package.json | 2 +- packages/stats/app/package.json | 2 +- packages/stats/core/package.json | 2 +- packages/stats/server/package.json | 2 +- packages/tui/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 30 files changed, 57 insertions(+), 57 deletions(-) diff --git a/bun.lock b/bun.lock index ccca966458..426438cfbb 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", @@ -96,7 +96,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.18.4", + "version": "1.18.5", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -144,7 +144,7 @@ }, "packages/codemode": { "name": "@opencode-ai/codemode", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "acorn": "8.15.0", "effect": "catalog:", @@ -158,7 +158,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -194,7 +194,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -221,7 +221,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -243,7 +243,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -267,7 +267,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -287,7 +287,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.18.4", + "version": "1.18.5", "bin": { "opencode": "./bin/opencode", }, @@ -381,7 +381,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@zip.js/zip.js": "2.7.62", "effect": "catalog:", @@ -435,7 +435,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -449,7 +449,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "effect": "catalog:", }, @@ -461,7 +461,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -493,7 +493,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -509,7 +509,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83", @@ -540,7 +540,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", @@ -559,7 +559,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.18.4", + "version": "1.18.5", "bin": { "opencode": "./bin/opencode", }, @@ -690,7 +690,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "workspace:*", @@ -766,7 +766,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "cross-spawn": "catalog:", }, @@ -781,7 +781,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", @@ -796,7 +796,7 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13.tgz", @@ -841,7 +841,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -854,7 +854,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@ibm/plex": "6.4.1", "@kobalte/core": "catalog:", @@ -888,7 +888,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -907,7 +907,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -949,7 +949,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -976,7 +976,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1027,7 +1027,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.18.4", + "version": "1.18.5", "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 f6a1bf9d2e..23a830fcd5 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.18.4", + "version": "1.18.5", "description": "", "type": "module", "exports": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 58b1429ad5..26a6210281 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/cli", - "version": "1.18.4", + "version": "1.18.5", "type": "module", "license": "MIT", "bin": { diff --git a/packages/codemode/package.json b/packages/codemode/package.json index 80c8f5679d..15f3910e27 100644 --- a/packages/codemode/package.json +++ b/packages/codemode/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/codemode", - "version": "1.18.4", + "version": "1.18.5", "description": "Effect-native confined code execution over schema-described tools", "private": true, "type": "module", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 6c6f14e20a..22e440d0d5 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.18.4", + "version": "1.18.5", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 6a04328e79..be70f690c4 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.18.4", + "version": "1.18.5", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index a7df87572a..5909638d05 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.18.4", + "version": "1.18.5", "$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 ac5932cb07..ca8f1dada9 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.18.4", + "version": "1.18.5", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/console/support/package.json b/packages/console/support/package.json index 0da6c16520..3fb6d2fdf2 100644 --- a/packages/console/support/package.json +++ b/packages/console/support/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-support", - "version": "1.18.4", + "version": "1.18.5", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index 761bee109a..7ecdd92632 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.4", + "version": "1.18.5", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 78e070c4c0..9820c107cd 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.18.4", + "version": "1.18.5", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index 7b3df31b8d..d29305faae 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.4", + "version": "1.18.5", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/effect-sqlite-node/package.json b/packages/effect-sqlite-node/package.json index 093b913a63..e56d3ba4f2 100644 --- a/packages/effect-sqlite-node/package.json +++ b/packages/effect-sqlite-node/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.4", + "version": "1.18.5", "name": "@opencode-ai/effect-sqlite-node", "type": "module", "license": "MIT", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 1e0c5e6e0c..761f1c7589 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.18.4", + "version": "1.18.5", "private": true, "type": "module", "license": "MIT", diff --git a/packages/function/package.json b/packages/function/package.json index be1eba336f..535ee6faa6 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.18.4", + "version": "1.18.5", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index 9bcbf3839a..a39ac13642 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.4", + "version": "1.18.5", "name": "@opencode-ai/http-recorder", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "type": "module", diff --git a/packages/llm/package.json b/packages/llm/package.json index dc1eda59e4..6afa905bb4 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.4", + "version": "1.18.5", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 0876f4badb..7960a72dfe 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.4", + "version": "1.18.5", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 2b7bc94639..fdeab9b82b 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.18.4", + "version": "1.18.5", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 261d9a54d7..093439705a 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.18.4", + "version": "1.18.5", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 578217a703..d011dbb473 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/server", - "version": "1.18.4", + "version": "1.18.5", "private": true, "type": "module", "license": "MIT", diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index 3cf74b043b..158cb21e0a 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/session-ui", - "version": "1.18.4", + "version": "1.18.5", "private": true, "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index 3dd5746db5..e4fc453450 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.18.4", + "version": "1.18.5", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json index 84ff0a214d..b14e49e2d8 100644 --- a/packages/stats/app/package.json +++ b/packages/stats/app/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-app", - "version": "1.18.4", + "version": "1.18.5", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/core/package.json b/packages/stats/core/package.json index e828912704..f621709185 100644 --- a/packages/stats/core/package.json +++ b/packages/stats/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-core", - "version": "1.18.4", + "version": "1.18.5", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/server/package.json b/packages/stats/server/package.json index 7daf57998b..e679540583 100644 --- a/packages/stats/server/package.json +++ b/packages/stats/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-server", - "version": "1.18.4", + "version": "1.18.5", "private": true, "type": "module", "license": "MIT", diff --git a/packages/tui/package.json b/packages/tui/package.json index 3fd4693780..d450262d85 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/tui", - "version": "1.18.4", + "version": "1.18.5", "private": true, "type": "module", "license": "MIT", diff --git a/packages/ui/package.json b/packages/ui/package.json index b8e3ec7172..971c3c89d0 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.18.4", + "version": "1.18.5", "type": "module", "license": "MIT", "repository": { diff --git a/packages/web/package.json b/packages/web/package.json index 36ce8f81f9..2918b28f39 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.18.4", + "version": "1.18.5", "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 a5c257f712..4e89e1f080 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.18.4", + "version": "1.18.5", "publisher": "sst-dev", "repository": { "type": "git", From 065dc274ec46a9995c04d8908293d3502bcff67e Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 24 Jul 2026 21:23:44 -0400 Subject: [PATCH 006/129] fix(core): branch-keyed repository cache with gated reference readiness (#38759) --- packages/core/src/reference.ts | 6 +- packages/core/src/repository-cache.ts | 80 +++++++++---------- packages/core/src/repository.ts | 10 ++- packages/core/test/reference.test.ts | 2 +- packages/core/test/repository-cache.test.ts | 37 ++++++++- packages/core/test/repository.test.ts | 6 ++ .../test/server/httpapi-reference.test.ts | 2 +- 7 files changed, 90 insertions(+), 53 deletions(-) diff --git a/packages/core/src/reference.ts b/packages/core/src/reference.ts index 5303dbd955..1e1ab9d20f 100644 --- a/packages/core/src/reference.ts +++ b/packages/core/src/reference.ts @@ -58,7 +58,6 @@ const layer = Layer.effect( finalize: (draft) => Effect.gen(function* () { materialized.clear() - const seen = new Map() for (const [name, source] of draft.list()) { if (source.type === "local") { materialized.set( @@ -82,14 +81,11 @@ const layer = Layer.effect( continue } } - const target = Repository.cachePath(global.repos, repository) - if (seen.has(target) && seen.get(target) !== source.branch) continue - seen.set(target, source.branch) materialized.set( name, new Info({ name, - path: AbsolutePath.make(target), + path: AbsolutePath.make(Repository.cachePath(global.repos, repository, source.branch)), ...(source.description === undefined ? {} : { description: source.description }), ...(source.hidden === undefined ? {} : { hidden: source.hidden }), source, diff --git a/packages/core/src/repository-cache.ts b/packages/core/src/repository-cache.ts index 988236743b..f166219125 100644 --- a/packages/core/src/repository-cache.ts +++ b/packages/core/src/repository-cache.ts @@ -1,3 +1,10 @@ +/** + * Local tracking checkouts for remote Git references, one per remote and + * branch. Each checkout permanently tracks a single ref: the requested branch + * when the cache key has one, otherwise the remote default branch. Content + * follows "newest wins": refresh fetches and hard-resets, so readers may + * observe the checkout move underneath them. + */ import path from "path" import { Context, Effect, Layer, Schema } from "effect" import { FSUtil } from "./fs-util" @@ -135,7 +142,7 @@ const layer: Layer.Layer new FetchFailedError({ repository, message: error.message }))) if (input.branch) { - const requestedBranch = input.branch yield* git.sync - .fetchBranch(existing, { branch: requestedBranch }) + .fetchBranch(existing, { branch: input.branch }) .pipe(Effect.mapError((error) => new FetchFailedError({ repository, message: error.message }))) - - yield* git.sync.checkoutRemoteBranch(existing, { branch: requestedBranch }).pipe( - Effect.mapError( - (error) => - new CheckoutFailedError({ - repository, - branch: requestedBranch, - message: error.message, - }), - ), - ) } + // Checking out the tracked ref before resetting keeps the + // checkout self-healing even if it was left on another + // branch. + const branch = input.branch ?? (yield* git.history.defaultRemoteBranch(existing)) + if (branch) { + yield* git.sync + .checkoutRemoteBranch(existing, { branch }) + .pipe( + Effect.mapError( + (error) => new CheckoutFailedError({ repository, branch, message: error.message }), + ), + ) + } + + const target = branch ?? (yield* git.history.branch(existing)) yield* git.sync - .resetHard(existing, yield* resetTarget(git, existing, input.branch)) + .resetHard(existing, target ? `origin/${target}` : "HEAD") .pipe(Effect.mapError((error) => new ResetFailedError({ repository, message: error.message }))) } @@ -229,12 +242,6 @@ export const node = makeGlobalNode({ deps: [EffectFlock.node, FSUtil.node, Git.node, Global.node], }) -function statusForRepository(input: { reuse: boolean; refresh?: boolean; branchMatches?: boolean }) { - if (!input.reuse) return "cloned" as const - if (input.branchMatches === false || input.refresh) return "refreshed" as const - return "cached" as const -} - function errorMessage(error: unknown) { return error instanceof globalThis.Error ? error.message : String(error) } @@ -245,17 +252,4 @@ function cacheOperation(effect: Effect.Effect, operation: stri ) } -const resetTarget = Effect.fnUntraced(function* ( - git: Git.Interface, - repository: Git.Repository, - requestedBranch?: string, -) { - if (requestedBranch) return `origin/${requestedBranch}` - const remoteHead = yield* git.history.defaultRemoteBranch(repository) - if (remoteHead) return `origin/${remoteHead}` - const currentBranch = yield* git.history.branch(repository) - if (currentBranch) return `origin/${currentBranch}` - return "HEAD" -}) - export * as RepositoryCache from "./repository-cache" diff --git a/packages/core/src/repository.ts b/packages/core/src/repository.ts index dbc6a8fbca..8ee5be600e 100644 --- a/packages/core/src/repository.ts +++ b/packages/core/src/repository.ts @@ -118,8 +118,14 @@ export function isRemote(reference: Reference): reference is RemoteReference { return !isFile(reference) } -export function cachePath(root: string, reference: Reference): string { - return path.join(root, ...reference.host.split(":"), ...reference.segments) +/** + * Checkouts are keyed by remote and branch: a branch-specific reference gets + * its own directory so branchless refreshes can never move it. The branch is + * percent-encoded because valid branch names may contain `/`. + */ +export function cachePath(root: string, reference: Reference, branch?: string): string { + const base = path.join(root, ...reference.host.split(":"), ...reference.segments) + return branch ? `${base}@${encodeURIComponent(branch)}` : base } export function cacheIdentity(reference: Reference): string { diff --git a/packages/core/test/reference.test.ts b/packages/core/test/reference.test.ts index db61232310..5ab215230a 100644 --- a/packages/core/test/reference.test.ts +++ b/packages/core/test/reference.test.ts @@ -47,7 +47,7 @@ describe("Reference", () => { expect(yield* references.list()).toEqual([ new Reference.Info({ name: "sdk", - path: AbsolutePath.make(Repository.cachePath(Global.Path.repos, repository)), + path: AbsolutePath.make(Repository.cachePath(Global.Path.repos, repository, "main")), source, }), ]) diff --git a/packages/core/test/repository-cache.test.ts b/packages/core/test/repository-cache.test.ts index 2dd0ce2502..4433583bf7 100644 --- a/packages/core/test/repository-cache.test.ts +++ b/packages/core/test/repository-cache.test.ts @@ -8,7 +8,7 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Global } from "@opencode-ai/core/global" import { Repository } from "@opencode-ai/core/repository" import { RepositoryCache } from "@opencode-ai/core/repository-cache" -import { git, gitRemote } from "./fixture/git" +import { branch, git, gitRemote } from "./fixture/git" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" @@ -66,6 +66,41 @@ describe("RepositoryCache", () => { ), ) + it.live("keeps branch checkouts isolated from branchless refreshes", () => + withRemote((fixture) => + Effect.gen(function* () { + yield* Effect.promise(() => branch(fixture.source, "feature", "two\n")) + const cache = yield* RepositoryCache.Service + + const featured = yield* cache.ensure({ reference: fixture.reference, branch: "feature" }) + expect(featured.branch).toBe("feature") + expect(featured.localPath.endsWith("repo@feature")).toBe(true) + expect(yield* read(path.join(featured.localPath, "README.md"))).toBe("two\n") + + const refreshed = yield* cache.ensure({ reference: fixture.reference, refresh: true }) + expect(refreshed.localPath).not.toBe(featured.localPath) + expect(yield* read(path.join(refreshed.localPath, "README.md"))).toBe("one\n") + + const cached = yield* cache.ensure({ reference: fixture.reference, branch: "feature" }) + expect(cached.status).toBe("cached") + expect(yield* read(path.join(cached.localPath, "README.md"))).toBe("two\n") + }).pipe(Effect.provide(cacheLayer(fixture.root))), + ), + ) + + it.live("does not mistake an enclosing repository for the cache checkout", () => + withRemote((fixture) => + Effect.gen(function* () { + yield* Effect.promise(() => git(fixture.root, "clone", fixture.remote, path.join(fixture.root, "repos"))) + + const result = yield* (yield* RepositoryCache.Service).ensure({ reference: fixture.reference }) + + expect(result.status).toBe("cloned") + expect(yield* read(path.join(result.localPath, "README.md"))).toBe("one\n") + }).pipe(Effect.provide(cacheLayer(fixture.root))), + ), + ) + it.live("returns typed validation and clone failures", () => withRemote((fixture) => Effect.gen(function* () { diff --git a/packages/core/test/repository.test.ts b/packages/core/test/repository.test.ts index 5b18f8b1d6..1a6af675d1 100644 --- a/packages/core/test/repository.test.ts +++ b/packages/core/test/repository.test.ts @@ -17,6 +17,12 @@ describe("Repository", () => { label: "owner/repo", }) expect(Repository.cachePath("/cache", reference)).toBe(path.join("/cache", "github.com", "owner", "repo")) + expect(Repository.cachePath("/cache", reference, "main")).toBe( + path.join("/cache", "github.com", "owner", "repo@main"), + ) + expect(Repository.cachePath("/cache", reference, "feature/x")).toBe( + path.join("/cache", "github.com", "owner", "repo@feature%2Fx"), + ) expect(Repository.cacheIdentity(reference)).toBe("github.com/owner/repo") }) diff --git a/packages/opencode/test/server/httpapi-reference.test.ts b/packages/opencode/test/server/httpapi-reference.test.ts index 9354d527da..b187bd4eea 100644 --- a/packages/opencode/test/server/httpapi-reference.test.ts +++ b/packages/opencode/test/server/httpapi-reference.test.ts @@ -51,7 +51,7 @@ describe("reference HttpApi", () => { }, { name: "effect", - path: path.join(Global.Path.repos, "github.com", "Effect-TS", "effect"), + path: path.join(Global.Path.repos, "github.com", "Effect-TS", "effect@main"), source: { type: "git", repository: "Effect-TS/effect", From 5e2a6257b22c0141a20c281f4c2a641311afe5a5 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 25 Jul 2026 01:25:04 +0000 Subject: [PATCH 007/129] chore: generate --- packages/core/src/repository-cache.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/src/repository-cache.ts b/packages/core/src/repository-cache.ts index f166219125..cab2f631ff 100644 --- a/packages/core/src/repository-cache.ts +++ b/packages/core/src/repository-cache.ts @@ -167,7 +167,11 @@ const layer: Layer.Layer Date: Sat, 25 Jul 2026 14:58:05 +0800 Subject: [PATCH 008/129] fix(app): refresh V1 providers after auth (#38786) Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Brendan Allan --- .../components/dialog-connect-provider.tsx | 6 +-- .../app/src/components/settings-providers.tsx | 2 +- .../settings-v2/dialog-settings-v2.tsx | 20 +++++++- .../src/components/settings-v2/providers.tsx | 11 ++-- packages/app/src/context/models.tsx | 2 +- packages/app/src/hooks/use-providers.ts | 10 ++-- packages/app/src/pages/layout.tsx | 2 +- .../composer/session-composer-controls.ts | 50 ++++++++++--------- packages/app/src/utils/server-compat.test.ts | 37 ++++++++++++++ packages/app/src/utils/server-compat.ts | 6 +++ 10 files changed, 107 insertions(+), 39 deletions(-) diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 9ad389317b..744bc79227 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -159,7 +159,7 @@ function ProviderPicker(props: { const settings = useSettings() if (settings.general.newLayoutDesigns()) return - const providers = useProviders(props.directory) + const providers = useProviders(() => props.directory?.()) const language = useLanguage() const popularGroup = () => language.t("dialog.provider.group.popular") const otherGroup = () => language.t("dialog.provider.group.other") @@ -231,7 +231,7 @@ function ProviderPickerV2(props: { onSelect: (provider: string) => void onPrepare?: () => void }) { - const providers = useProviders(props.directory) + const providers = useProviders(() => props.directory?.()) const language = useLanguage() const [store, setStore] = createStore({ filter: "", @@ -391,7 +391,7 @@ function ProviderConnection(props: { const language = useLanguage() const settings = useSettings() const newLayout = settings.general.newLayoutDesigns - const providers = useProviders(props.directory) + const providers = useProviders(() => props.directory?.()) const directory = () => props.directory?.() ?? decode64(params.dir) const location = () => { const value = directory() diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 7a15d82eaf..080e6c517b 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -41,7 +41,7 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) => const serverSDK = useServerSDK() const protocol = useServerProtocol() const serverSync = useServerSync() - const providers = useProviders() + const providers = useProviders(() => undefined) const providerConnect = useProviderConnectController({ onBack: props.onBack }) const connect = (provider?: string) => { diff --git a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx index af24a47274..4116f4a620 100644 --- a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx +++ b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx @@ -1,4 +1,4 @@ -import { Component, createSignal, startTransition } from "solid-js" +import { Component, createMemo, createSignal, startTransition } from "solid-js" import { Dialog } from "@opencode-ai/ui/v2/dialog-v2" import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2" import { Icon } from "@opencode-ai/ui/icon" @@ -11,6 +11,9 @@ import { SettingsModelsV2 } from "./models" import "./settings-v2.css" import { SettingsServersV2 } from "./servers" import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useLayout } from "@/context/layout" +import { useTabs } from "@/context/tabs" +import { useServerSync } from "@/context/server-sync" export const DialogSettings: Component<{ sessionID?: string @@ -19,7 +22,20 @@ export const DialogSettings: Component<{ const language = useLanguage() const platform = usePlatform() const dialog = useDialog() + const layout = useLayout() + const tabs = useTabs() + const serverSync = useServerSync() const [tab, setTab] = createSignal(props.defaultValue ?? "general") + const directory = createMemo(() => { + const route = layout.route() + if (route.type === "dir-new-sesssion") return route.dir + if (route.type === "draft") { + const draft = tabs.store.find((item) => item.type === "draft" && item.draftID === route.draftID) + return draft?.type === "draft" ? draft.directory : undefined + } + if (route.type === "session") return serverSync().session.get(route.sessionId)?.directory + return undefined + }) const showProviders = () => { void dialog.show(() => ) @@ -87,7 +103,7 @@ export const DialogSettings: Component<{ - + diff --git a/packages/app/src/components/settings-v2/providers.tsx b/packages/app/src/components/settings-v2/providers.tsx index 29192114f0..acd73ddbe5 100644 --- a/packages/app/src/components/settings-v2/providers.tsx +++ b/packages/app/src/components/settings-v2/providers.tsx @@ -4,7 +4,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { showToast } from "@/utils/toast" import { popularProviders, useProviders } from "@/hooks/use-providers" -import { createMemo, type Component, For, Show } from "solid-js" +import { createMemo, type Accessor, type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" import { useServerProtocol, useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" @@ -29,18 +29,21 @@ const PROVIDER_NOTES = [ const PROVIDER_ICON_SIZE = 16 -export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) => { +export const SettingsProvidersV2: Component<{ + directory: Accessor + onBack?: () => void +}> = (props) => { const dialog = useDialog() const language = useLanguage() const serverSdk = useServerSDK() const protocol = useServerProtocol() const serverSync = useServerSync() - const providers = useProviders() + const providers = useProviders(props.directory) const providerConnect = useProviderConnectController({ onBack: props.onBack }) const connect = (provider?: string) => { providerConnect.select(provider) - void dialog.show(() => ) + void dialog.show(() => ) } const connected = createMemo(() => { diff --git a/packages/app/src/context/models.tsx b/packages/app/src/context/models.tsx index 736dc3a7c5..a80cf2e580 100644 --- a/packages/app/src/context/models.tsx +++ b/packages/app/src/context/models.tsx @@ -26,7 +26,7 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext( name: "Models", gate: false, init: (props: { directory?: Accessor } = {}) => { - const providers = useProviders(props.directory) + const providers = useProviders(() => props.directory?.()) const [store, setStore, _, ready] = persisted( Persist.global("model", ["model.v1"]), diff --git a/packages/app/src/hooks/use-providers.ts b/packages/app/src/hooks/use-providers.ts index 982c26ca97..60ad01c867 100644 --- a/packages/app/src/hooks/use-providers.ts +++ b/packages/app/src/hooks/use-providers.ts @@ -2,7 +2,7 @@ import { useServerSync } from "@/context/server-sync" import { decode64 } from "@/utils/base64" import { useParams } from "@solidjs/router" import { Iterable, pipe } from "effect" -import type { Accessor } from "solid-js" +import { createEffect, createMemo, type Accessor } from "solid-js" import { selectProviderCatalog } from "./provider-catalog" export const popularProviders = [ @@ -17,14 +17,14 @@ export const popularProviders = [ ] const popularProviderSet = new Set(popularProviders) -export function useProviders(directory?: Accessor) { +export function useProviders(directory: Accessor) { const serverSync = useServerSync() const params = useParams() const dir = () => (directory ? directory() : decode64(params.dir)) const providers = () => { const value = dir() const projectStore = value ? serverSync().child(value)[0] : undefined - if (directory) + if (value) return selectProviderCatalog({ explicit: true, directory: value, @@ -37,6 +37,7 @@ export function useProviders(directory?: Accessor) { global: serverSync().data.provider, }) } + return { all: () => providers().all, default: () => providers().default, @@ -58,7 +59,7 @@ export function useProviders(directory?: Accessor) { }, paid: () => { const connected = new Set(providers().connected) - return [ + const paid = [ ...Iterable.filter( providers().all, ([id]) => @@ -66,6 +67,7 @@ export function useProviders(directory?: Accessor) { (id !== "opencode" || Object.values(providers().all.get(id)?.models ?? {}).some((m) => m.cost?.input)), ), ] + return paid }, } } diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 5947442318..96ed022373 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -119,7 +119,7 @@ export default function LegacyLayout(props: ParentProps) { const permission = usePermission() const navigate = useNavigate() setNavigate(navigate) - const providers = useProviders() + const providers = useProviders(() => undefined) const dialog = useDialog() const command = useCommand() const theme = useTheme() diff --git a/packages/app/src/pages/session/composer/session-composer-controls.ts b/packages/app/src/pages/session/composer/session-composer-controls.ts index 4ae7827e21..a9b0070bc0 100644 --- a/packages/app/src/pages/session/composer/session-composer-controls.ts +++ b/packages/app/src/pages/session/composer/session-composer-controls.ts @@ -25,35 +25,39 @@ export function createPromptInputController(input: { }) { const layout = useLayout() const local = useLocal() - const providers = useProviders() - const sync = useSync() const sdk = useSDK() + const sync = useSync() + const providers = useProviders(() => sdk().directory) const view = layout.view(input.sessionKey) const agentsQuery = createQuery(() => input.queryOptions.agents(pathKey(sdk().directory))) const globalProvidersQuery = createQuery(() => input.queryOptions.providers(null)) const providersQuery = createQuery(() => input.queryOptions.providers(pathKey(sdk().directory))) - return createMemo(() => ({ - agents: { - available: sync().data.agent, - options: local.agent.list().map((agent) => agent.name), - current: local.agent.current()?.name ?? "", - loading: agentsQuery.isLoading, - visible: local.agent.visible(), - select: local.agent.set, - }, - model: { - selection: input.model ?? local.model, - paid: providers.paid().length > 0, - loading: - (local.agent.visible() && agentsQuery.isLoading) || providersQuery.isLoading || globalProvidersQuery.isLoading, - }, - session: { - id: input.sessionID(), - tabs: layout.tabs(input.sessionKey), - reviewPanel: view.reviewPanel, - }, - })) + return createMemo(() => { + return { + agents: { + available: sync().data.agent, + options: local.agent.list().map((agent) => agent.name), + current: local.agent.current()?.name ?? "", + loading: agentsQuery.isLoading, + visible: local.agent.visible(), + select: local.agent.set, + }, + model: { + selection: input.model ?? local.model, + paid: providers.paid().length > 0, + loading: + (local.agent.visible() && agentsQuery.isLoading) || + providersQuery.isLoading || + globalProvidersQuery.isLoading, + }, + session: { + id: input.sessionID(), + tabs: layout.tabs(input.sessionKey), + reviewPanel: view.reviewPanel, + }, + } + }) } export function createPromptProjectControls() { diff --git a/packages/app/src/utils/server-compat.test.ts b/packages/app/src/utils/server-compat.test.ts index 3f4b8f2205..605fdaace8 100644 --- a/packages/app/src/utils/server-compat.test.ts +++ b/packages/app/src/utils/server-compat.test.ts @@ -190,4 +190,41 @@ describe("createCompatibleApi", () => { expect(new URL(requests[0]!.url).pathname).toBe("/session/ses_1/permissions/permission_1") expect(new URL(requests[0]!.url).searchParams.get("directory")).toBe("/other") }) + + test("disposes the V1 instance after connecting a provider", async () => { + const { api, requests } = setup("v1") + + await api.integration.connect.key({ + integrationID: "openrouter", + key: "secret", + location: { directory: "/repo" }, + }) + + expect(requests.map((request) => new URL(request.url).pathname)).toEqual([ + "/auth/openrouter", + "/instance/dispose", + "/instance/dispose", + ]) + expect(requests[1]!.headers.get("x-opencode-directory")).toBe("%2Frepo") + expect(requests[2]!.headers.get("x-opencode-directory")).toBeNull() + }) + + test("disposes the V1 instance after completing provider OAuth", async () => { + const { api, requests } = setup("v1") + + await api.integration.oauth.complete({ + integrationID: "openrouter", + attemptID: "openrouter:0", + code: "code", + location: { directory: "/repo" }, + }) + + expect(requests.map((request) => new URL(request.url).pathname)).toEqual([ + "/provider/openrouter/oauth/callback", + "/instance/dispose", + "/instance/dispose", + ]) + expect(requests[1]!.headers.get("x-opencode-directory")).toBe("%2Frepo") + expect(requests[2]!.headers.get("x-opencode-directory")).toBeNull() + }) }) diff --git a/packages/app/src/utils/server-compat.ts b/packages/app/src/utils/server-compat.ts index 72e74438ed..94374aa9f9 100644 --- a/packages/app/src/utils/server-compat.ts +++ b/packages/app/src/utils/server-compat.ts @@ -401,6 +401,8 @@ function createV1Api(input: CompatibleInput): CompatibleApi { providerID: value.integrationID, auth: { type: "api", key: value.key }, }) + await legacy(value.location).instance.dispose() + await input.legacy().instance.dispose() }, }, oauth: { @@ -429,6 +431,8 @@ function createV1Api(input: CompatibleInput): CompatibleApi { { providerID: value.integrationID, method, code: value.code }, { throwOnError: true }, ) + await legacy(value.location).instance.dispose() + await input.legacy().instance.dispose() }, status: async (value: Parameters[0]) => { const method = Number(value.attemptID.split(":").at(-1)) @@ -436,6 +440,8 @@ function createV1Api(input: CompatibleInput): CompatibleApi { { providerID: value.integrationID, method }, { throwOnError: true }, ) + await legacy(value.location).instance.dispose() + await input.legacy().instance.dispose() return located( { status: "complete" as const, time: { created: Date.now(), expires: Date.now() } }, value.location, From 2b2b69d668ed05836ea6d3fa7f42d416bdb61806 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Sat, 25 Jul 2026 15:01:18 +0800 Subject: [PATCH 009/129] fix(app): refresh V1 MCP state (#38816) --- .../src/context/global-sync/bootstrap.test.ts | 16 +++++++++++++--- .../app/src/context/global-sync/bootstrap.ts | 11 +++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/app/src/context/global-sync/bootstrap.test.ts b/packages/app/src/context/global-sync/bootstrap.test.ts index de2baa704c..58cc65a2e0 100644 --- a/packages/app/src/context/global-sync/bootstrap.test.ts +++ b/packages/app/src/context/global-sync/bootstrap.test.ts @@ -73,14 +73,14 @@ function directoryState() { } describe("bootstrapDirectory", () => { - test("marks a loading directory partial during bootstrap and complete after success", async () => { + test("uses legacy MCP endpoints while refreshing a v1 directory", async () => { const mcpReads: string[] = [] const [store, setStore] = directoryState() await bootstrapDirectory({ directory: "/project", scope: ServerScope.local, - mcp: false, + mcp: true, global: { config: {} satisfies Config, path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" }, @@ -90,6 +90,7 @@ describe("bootstrapDirectory", () => { sdk: { app: { agents: async () => ({ data: [{ name: "build", mode: "primary" }] }) }, config: { get: async () => ({ data: {} }) }, + session: { status: async () => ({ data: {} }) }, vcs: { get: async () => ({ data: undefined }) }, command: { list: async () => { @@ -106,6 +107,14 @@ describe("bootstrapDirectory", () => { return { data: {} } }, }, + experimental: { + resource: { + list: async () => { + mcpReads.push("resource") + return { data: {} } + }, + }, + }, provider: { list: async () => ({ data: { all: [], connected: [], default: {} } }) }, } as unknown as OpencodeClient, api, @@ -115,6 +124,7 @@ describe("bootstrapDirectory", () => { loadSessions() {}, translate: (key) => key, queryClient: new QueryClient(), + protocol: Promise.resolve("v1"), }) expect(store.status).toBe("partial") @@ -122,7 +132,7 @@ describe("bootstrapDirectory", () => { await new Promise((resolve) => setTimeout(resolve, 80)) expect(store.status).toBe("complete") - expect(mcpReads).toEqual([]) + expect(mcpReads.sort()).toEqual(["command", "resource", "status"]) }) }) diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 4f7b949e61..cf030c8af0 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -503,9 +503,16 @@ export async function bootstrapDirectory(input: { }), ), () => Promise.resolve(input.loadSessions(input.directory)), - input.mcp && (() => input.queryClient.fetchQuery(loadMcpQuery(input.scope, input.directory, input.api.mcp))), input.mcp && - (() => input.queryClient.fetchQuery(loadMcpResourcesQuery(input.scope, input.directory, input.api.mcp))), + (() => + input.queryClient.fetchQuery( + loadMcpQuery(input.scope, input.directory, input.api.mcp, input.sdk, input.protocol), + )), + input.mcp && + (() => + input.queryClient.fetchQuery( + loadMcpResourcesQuery(input.scope, input.directory, input.api.mcp, input.sdk, input.protocol), + )), () => input.queryClient .fetchQuery(loadProvidersQuery(input.scope, input.directory, input.api, input.sdk, input.protocol)) From 0a6637e17aa79789a86608121f4dee8fad442d4f Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Sat, 25 Jul 2026 16:07:13 +0800 Subject: [PATCH 010/129] chore(app): vendor v2 client snapshot (#38818) --- bun.lock | 8 +-- .../review-state-persistence.spec.ts | 16 ++--- .../review-terminal-stacked.spec.ts | 46 ++++++------ .../regression/terminal-tab-switch.spec.ts | 2 +- packages/app/package.json | 2 +- .../components/dialog-select-directory-v2.tsx | 9 ++- .../components/dialog-select-directory.tsx | 9 ++- packages/app/src/components/edit-project.ts | 23 ++++-- packages/app/src/components/prompt-input.tsx | 2 +- .../app/src/components/settings-general.tsx | 3 +- .../src/components/settings-v2/general.tsx | 3 +- packages/app/src/components/terminal.tsx | 22 +++--- packages/app/src/context/directory-sync.ts | 3 +- .../src/context/global-sync/bootstrap.test.ts | 8 ++- .../app/src/context/global-sync/bootstrap.ts | 47 +++++++------ .../src/context/global-sync/event-reducer.ts | 26 +++---- packages/app/src/context/layout.tsx | 26 ++++--- .../context/server-session-v2-reducer.test.ts | 2 +- .../src/context/server-session-v2-reducer.ts | 21 ++++-- .../app/src/context/server-session.test.ts | 5 +- packages/app/src/context/server-session.ts | 13 ++-- packages/app/src/context/server-sync.test.ts | 4 +- packages/app/src/context/server-sync.tsx | 5 +- .../pages/home/home-sessions-controller.tsx | 8 ++- packages/app/src/pages/layout.tsx | 38 +++++++--- .../session/timeline/message-timeline.tsx | 3 +- packages/app/src/utils/server-compat.test.ts | 24 ++++--- packages/app/src/utils/server-compat.ts | 66 +++++++++--------- .../app/src/utils/session-message.test.ts | 4 +- packages/app/src/utils/session-message.ts | 9 ++- .../vendor/opencode-ai-client-1.17.13-v2.tgz | Bin 0 -> 67724 bytes .../app/vendor/opencode-ai-client-1.17.13.tgz | Bin 75585 -> 0 bytes packages/session-ui/package.json | 2 +- 33 files changed, 268 insertions(+), 191 deletions(-) create mode 100644 packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz delete mode 100644 packages/app/vendor/opencode-ai-client-1.17.13.tgz diff --git a/bun.lock b/bun.lock index 426438cfbb..839f288394 100644 --- a/bun.lock +++ b/bun.lock @@ -37,7 +37,7 @@ "@dnd-kit/helpers": "0.5.0", "@dnd-kit/solid": "0.5.0", "@kobalte/core": "catalog:", - "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13.tgz", + "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", @@ -799,7 +799,7 @@ "version": "1.18.5", "dependencies": { "@kobalte/core": "catalog:", - "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13.tgz", + "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -6030,7 +6030,7 @@ "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], - "@opencode-ai/app/@opencode-ai/client": ["@opencode-ai/client@vendor/opencode-ai-client-1.17.13.tgz", {}, "sha512-1cYJikTCrXNhnS2qQ3P3rtdbGqhvJKksswG0amJNqgaeUfz3xDlKEDx+YoIosT7Cqk/AtO18jtip0lYL3TVdHQ=="], + "@opencode-ai/app/@opencode-ai/client": ["@opencode-ai/client@vendor/opencode-ai-client-1.17.13-v2.tgz", {}, "sha512-332kgNifvpQOF9e3UA+pIa5xPrMhLaQkUiNiO+meS0Ba9HjSE6hfsWnEojMkD0DPSLqPP6rCF1dDoF7U0Y0OCQ=="], "@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="], @@ -6050,7 +6050,7 @@ "@opencode-ai/script/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "@opencode-ai/session-ui/@opencode-ai/client": ["@opencode-ai/client@../app/vendor/opencode-ai-client-1.17.13.tgz", {}, "sha512-1cYJikTCrXNhnS2qQ3P3rtdbGqhvJKksswG0amJNqgaeUfz3xDlKEDx+YoIosT7Cqk/AtO18jtip0lYL3TVdHQ=="], + "@opencode-ai/session-ui/@opencode-ai/client": ["@opencode-ai/client@../app/vendor/opencode-ai-client-1.17.13-v2.tgz", {}, "sha512-332kgNifvpQOF9e3UA+pIa5xPrMhLaQkUiNiO+meS0Ba9HjSE6hfsWnEojMkD0DPSLqPP6rCF1dDoF7U0Y0OCQ=="], "@opencode-ai/session-ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], diff --git a/packages/app/e2e/regression/review-state-persistence.spec.ts b/packages/app/e2e/regression/review-state-persistence.spec.ts index 4f67756d53..0d6756201e 100644 --- a/packages/app/e2e/regression/review-state-persistence.spec.ts +++ b/packages/app/e2e/regression/review-state-persistence.spec.ts @@ -65,7 +65,7 @@ async function switchSession(page: Page, title: string) { async function setup(page: Page) { await mockOpenCodeServer(page, { - protocol: "v2", + protocol: "v1", directory, project: { id: projectID, @@ -93,20 +93,18 @@ async function setup(page: Page) { route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ location: { directory }, data: { branch: "feature", defaultBranch: "dev" } }), + body: JSON.stringify({ branch: "feature", default_branch: "dev" }), }), ) await page.route("**/vcs/diff**", (route) => route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ - location: { directory }, - data: - new URL(route.request().url()).searchParams.get("mode") === "branch" - ? [diff("src/alpha.ts"), diff("src/beta.ts")] - : [diff("src/alpha.ts"), diff("src/gamma.ts")], - }), + body: JSON.stringify( + new URL(route.request().url()).searchParams.get("mode") === "branch" + ? [diff("src/alpha.ts"), diff("src/beta.ts")] + : [diff("src/alpha.ts"), diff("src/gamma.ts")], + ), }), ) await page.addInitScript( diff --git a/packages/app/e2e/regression/review-terminal-stacked.spec.ts b/packages/app/e2e/regression/review-terminal-stacked.spec.ts index 7cc8723a8c..79b564820e 100644 --- a/packages/app/e2e/regression/review-terminal-stacked.spec.ts +++ b/packages/app/e2e/regression/review-terminal-stacked.spec.ts @@ -25,7 +25,7 @@ test("keeps the review tree and terminal sized when both panels are open", async let detailFailures = 1 await page.setViewportSize({ width: 1400, height: 900 }) await mockOpenCodeServer(page, { - protocol: "v2", + protocol: "v1", directory, project: { id: projectID, @@ -67,33 +67,31 @@ test("keeps the review tree and terminal sized when both panels are open", async status: 200, contentType: "application/json", body: JSON.stringify({ - location: { directory }, - data: { branch: "review-pane-performance", defaultBranch: "dev" }, + branch: "review-pane-performance", + default_branch: "dev", }), }), ) - await page.route("**/api/vcs/diff**", (route) => { + await page.route("**/vcs/diff**", (route) => { const url = new URL(route.request().url()) - const scope = url.searchParams.get("location[directory]")?.replaceAll("\\", "/") + const scope = url.searchParams.get("directory")?.replaceAll("\\", "/") const detail = scope?.endsWith("/src/branch/d00027") if (detail && detailFailures-- > 0) return route.fulfill({ status: 500, body: "retry detail" }) return route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ - location: { directory: scope ?? directory, project: { id: projectID, directory } }, - data: - url.searchParams.get("mode") === "branch" - ? detail - ? branchDiffs - .filter((diff) => diff.file.startsWith("src/branch/d00027/")) - .map((diff) => fileDiff(diff.file, diff.additions, true, detailVersion)) - : branchDiffs - : Array.from({ length: 7 }, (_, index) => fileDiff(`src/git-${index}.ts`, 1)), - }), + body: JSON.stringify( + url.searchParams.get("mode") === "branch" + ? detail + ? branchDiffs + .filter((diff) => diff.file.startsWith("src/branch/d00027/")) + .map((diff) => fileDiff(diff.file, diff.additions, true, detailVersion)) + : branchDiffs + : Array.from({ length: 7 }, (_, index) => fileDiff(`src/git-${index}.ts`, 1)), + ), }) }) - await page.route("**/api/pty*", (route) => + await page.route("**/pty*", (route) => route.fulfill({ status: 200, contentType: "application/json", @@ -111,7 +109,7 @@ test("keeps the review tree and terminal sized when both panels are open", async }), }), ) - await page.route("**/api/pty/pty_review_terminal*", (route) => + await page.route("**/pty/pty_review_terminal*", (route) => route.fulfill({ status: 200, contentType: "application/json", @@ -129,7 +127,7 @@ test("keeps the review tree and terminal sized when both panels are open", async }), }), ) - await page.route("**/api/pty/pty_review_terminal/connect-token*", (route) => + await page.route("**/pty/pty_review_terminal/connect-token*", (route) => route.fulfill({ status: 200, contentType: "application/json", @@ -139,7 +137,7 @@ test("keeps the review tree and terminal sized when both panels are open", async }), }), ) - await page.routeWebSocket("**/api/pty/pty_review_terminal/connect", () => undefined) + await page.routeWebSocket("**/pty/pty_review_terminal/connect", () => undefined) await page.addInitScript(() => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) localStorage.setItem( @@ -177,8 +175,8 @@ test("keeps the review tree and terminal sized when both panels are open", async const lazyDiff = page.waitForRequest((request) => { const url = new URL(request.url()) return ( - url.pathname === "/api/vcs/diff" && - url.searchParams.get("location[directory]")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true + url.pathname === "/vcs/diff" && + url.searchParams.get("directory")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true ) }) await lastFile.click() @@ -192,8 +190,8 @@ test("keeps the review tree and terminal sized when both panels are open", async const refreshedDiff = page.waitForRequest((request) => { const url = new URL(request.url()) return ( - url.pathname === "/api/vcs/diff" && - url.searchParams.get("location[directory]")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true + url.pathname === "/vcs/diff" && + url.searchParams.get("directory")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true ) }) sessionStatus[sessionID] = { type: "idle" } diff --git a/packages/app/e2e/regression/terminal-tab-switch.spec.ts b/packages/app/e2e/regression/terminal-tab-switch.spec.ts index 0076c5a2ca..165920753c 100644 --- a/packages/app/e2e/regression/terminal-tab-switch.spec.ts +++ b/packages/app/e2e/regression/terminal-tab-switch.spec.ts @@ -32,7 +32,7 @@ test("keeps the terminal session alive when switching session tabs in a workspac const connection = new URL(connections[0]!) expect(connection.pathname).toBe(`/api/pty/${ptyID}/connect`) expect(connection.searchParams.get("location[directory]")).toBe(directory) - expect(connection.searchParams.get("ticket")).toBe("e2e-ticket") + expect(connection.searchParams.get("ticket")).toBeNull() await writeProbe(page) await switchTab(page, titleB) diff --git a/packages/app/package.json b/packages/app/package.json index 23a830fcd5..75b2b8c488 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -53,7 +53,7 @@ "@dnd-kit/helpers": "0.5.0", "@dnd-kit/solid": "0.5.0", "@kobalte/core": "catalog:", - "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13.tgz", + "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", diff --git a/packages/app/src/components/dialog-select-directory-v2.tsx b/packages/app/src/components/dialog-select-directory-v2.tsx index e0909d849b..847239d098 100644 --- a/packages/app/src/components/dialog-select-directory-v2.tsx +++ b/packages/app/src/components/dialog-select-directory-v2.tsx @@ -8,6 +8,7 @@ import { createEffect, createMemo, createResource, createSignal, For, onCleanup, import { useGlobal } from "@/context/global" import { useLanguage } from "@/context/language" import { ServerConnection } from "@/context/server" +import type { Path } from "@opencode-ai/sdk/v2/client" import { absoluteTreePath, activeTreeNavigation, @@ -68,7 +69,13 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) { const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory)) const [fallbackPath] = createResource( () => (missingBase() ? true : undefined), - () => sdk.api.path.get().catch(() => undefined), + async (): Promise => { + if ((await sdk.protocol) !== "v1") return + return sdk.client.path + .get() + .then((result) => result.data) + .catch(() => undefined) + }, { initialValue: undefined }, ) const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "") diff --git a/packages/app/src/components/dialog-select-directory.tsx b/packages/app/src/components/dialog-select-directory.tsx index 5cc19fd920..4b24828e3e 100644 --- a/packages/app/src/components/dialog-select-directory.tsx +++ b/packages/app/src/components/dialog-select-directory.tsx @@ -9,6 +9,7 @@ import { useLanguage } from "@/context/language" import { ServerConnection } from "@/context/server" import { useGlobal } from "@/context/global" import { cleanPickerInput, createDirectorySearch, displayPickerPath } from "./directory-picker-domain" +import type { Path } from "@opencode-ai/sdk/v2/client" interface DialogSelectDirectoryProps { title?: string @@ -59,8 +60,12 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory)) const [fallbackPath] = createResource( () => (missingBase() ? true : undefined), - async () => { - return sdk.api.path.get().catch(() => undefined) + async (): Promise => { + if ((await sdk.protocol) !== "v1") return + return sdk.client.path + .get() + .then((result) => result.data) + .catch(() => undefined) }, { initialValue: undefined }, ) diff --git a/packages/app/src/components/edit-project.ts b/packages/app/src/components/edit-project.ts index 42053f6eff..0c5d576e93 100644 --- a/packages/app/src/components/edit-project.ts +++ b/packages/app/src/components/edit-project.ts @@ -71,12 +71,23 @@ export function createEditProjectModel(props: { project: LocalProject; server: S const start = store.startup.trim() if (props.project.id && props.project.id !== "global") { - const project = await serverCtx().sdk.api.project.update({ - projectID: props.project.id, - name, - icon: { color: store.color || "", override: store.iconOverride || "" }, - commands: { start }, - }) + if ((await serverCtx().sdk.protocol) !== "v1") return + const project = await serverCtx() + .sdk.client.project.update({ + projectID: props.project.id, + directory: props.project.worktree, + name, + icon: { color: store.color || "", override: store.iconOverride || "" }, + commands: { start }, + }) + .then((result) => result.data) + if (!project) return + // const project = await serverCtx().sdk.api.project.update({ + // projectID: props.project.id, + // name, + // icon: { color: store.color || "", override: store.iconOverride || "" }, + // commands: { start }, + // }) serverCtx().sync.set("project", (items) => items.map((item) => (item.id === project.id ? normalizeProjectInfo(project) : item)), ) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 3842b08791..f70cb39928 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -709,7 +709,7 @@ export const PromptInput: Component = (props) => { title: cmd.name, description: cmd.description, type: "custom" as const, - source: cmd.source, + // source: cmd.source, })) return [...custom, ...builtin] diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index 3ab265d729..a20e66de3e 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -132,7 +132,8 @@ export const SettingsGeneral: Component = () => { if ((await sdk.protocol) === "v1") { return (await sdk.client.pty.shells()).data ?? [] } - return (await sdk.api.pty.shells()).data + // return (await sdk.api.pty.shells()).data + return [] as ShellOption[] }, { initialValue: [] as ShellOption[] }, ) diff --git a/packages/app/src/components/settings-v2/general.tsx b/packages/app/src/components/settings-v2/general.tsx index 5a4cb186a6..613ac96cba 100644 --- a/packages/app/src/components/settings-v2/general.tsx +++ b/packages/app/src/components/settings-v2/general.tsx @@ -127,7 +127,8 @@ export const SettingsGeneralV2: Component<{ if ((await sdk.protocol) === "v1") { return (await sdk.client.pty.shells()).data ?? [] } - return (await sdk.api.pty.shells()).data + // return (await sdk.api.pty.shells()).data + return [] as ShellOption[] }, { initialValue: [] as ShellOption[] }, ) diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index b2e827f73a..6503503476 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -573,19 +573,13 @@ export const Terminal = (props: TerminalProps) => { throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.") throw new Error(`PTY connect ticket failed with ${result.response.status}`) } - return sdk() - .api.pty.connectToken({ - ptyID: id, - location: { directory }, - "x-opencode-ticket": "1", - }) - .then((result) => result.data.ticket) - .catch((err: unknown) => { - if (err && typeof err === "object" && "_tag" in err && err._tag === "ForbiddenError") { - throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.") - } - throw err - }) + // return sdk() + // .api.pty.connectToken({ + // ptyID: id, + // location: { directory }, + // "x-opencode-ticket": "1", + // }) + // .then((result) => result.data.ticket) } const retry = (err: unknown) => { @@ -616,7 +610,7 @@ export const Terminal = (props: TerminalProps) => { return undefined }) const protocol = await sdk().protocol - if (protocol === "v2" && !ticket) return + // if (protocol === "v2" && !ticket) return if (once.value) return if (disposed) return diff --git a/packages/app/src/context/directory-sync.ts b/packages/app/src/context/directory-sync.ts index ca6df85a53..befd5b61e0 100644 --- a/packages/app/src/context/directory-sync.ts +++ b/packages/app/src/context/directory-sync.ts @@ -134,7 +134,8 @@ export const createDirSyncContext = ( }, more: createMemo(() => current()[0].session.length >= current()[0].limit), archive: async (sessionID: string) => { - await serverSDK.api.session.archive({ sessionID, directory }) + if ((await serverSDK.protocol) !== "v1") return + await serverSDK.client.session.update({ sessionID, directory, time: { archived: Date.now() } }) current()[1]( "session", produce((draft) => { diff --git a/packages/app/src/context/global-sync/bootstrap.test.ts b/packages/app/src/context/global-sync/bootstrap.test.ts index 58cc65a2e0..71986dd3c4 100644 --- a/packages/app/src/context/global-sync/bootstrap.test.ts +++ b/packages/app/src/context/global-sync/bootstrap.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test" import { createStore } from "solid-js/store" import { QueryClient } from "@tanstack/solid-query" import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client" -import type { AgentApi, CatalogApi, CommandApi, ProjectApi, ReferenceApi } from "@opencode-ai/client/promise" +import type { AgentApi, CatalogApi, CommandApi, ReferenceApi } from "@opencode-ai/client/promise" import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" import { bootstrapDirectory, @@ -17,6 +17,8 @@ import type { State, VcsCache } from "./types" import { ServerScope } from "@/utils/server-scope" import type { ServerApi } from "@/utils/server" +type ProjectApi = ServerApi["project"] + const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse const api = { agent: { list: async () => ({ location: {}, data: [] }) }, @@ -200,7 +202,7 @@ describe("query keys", () => { calls.push(input) return { location: {}, - data: [{ name: "review", template: "Review files", source: "command" as const }], + data: [{ name: "review", template: "Review files" /* source: "command" as const */ }], } }, } as unknown as CommandApi @@ -208,7 +210,7 @@ describe("query keys", () => { const result = await loadCommands("/repo", api) expect(calls).toEqual([{ location: { directory: "/repo" } }]) - expect(result).toEqual([{ name: "review", template: "Review files", source: "command" }]) + expect(result).toEqual([{ name: "review", template: "Review files" /* source: "command" */ }]) }) test("loads projects from the current endpoint", async () => { diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index cf030c8af0..39221d551f 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -16,18 +16,12 @@ import type { CommandInfo, CommandListInput, CommandListOutput, - McpApi, - PathGetInput, - PathGetOutput, - PermissionApi, ProjectCurrentInput, ProjectCurrentOutput, ProjectListOutput, - QuestionApi, ReferenceListInput, ReferenceListOutput, SessionApi, - VcsApi, } from "@opencode-ai/client/promise" import { showToast } from "@/utils/toast" import { getFilename } from "@opencode-ai/core/util/path" @@ -50,6 +44,7 @@ import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" import { ScopedKey, type ServerScope } from "@/utils/server-scope" import { normalizeSessionInfo } from "@/utils/session" import type { ServerProtocol } from "@/utils/server-protocol" +import type { ServerApi } from "@/utils/server" type GlobalStore = { ready: boolean @@ -121,9 +116,10 @@ type ProjectApi = { readonly current: (input?: ProjectCurrentInput) => Promise } -type PathApi = { - readonly get: (input?: PathGetInput) => Promise -} +type McpApi = ServerApi["mcp"] +type PermissionApi = ServerApi["permission"] +type QuestionApi = ServerApi["question"] +type VcsApi = ServerApi["vcs"] export const loadProjectsQuery = (scope: ServerScope, api: ProjectApi) => queryOptions({ @@ -143,7 +139,7 @@ export const loadProjectsQuery = (scope: ServerScope, api: ProjectApi) => export async function bootstrapGlobal(input: { serverSDK: OpencodeClient - serverAPI: CatalogApi & { readonly path: PathApi; readonly project: ProjectApi } + serverAPI: CatalogApi & { readonly project: ProjectApi } protocol?: Promise scope: ServerScope requestFailedTitle: string @@ -158,7 +154,7 @@ export async function bootstrapGlobal(input: { input.queryClient.fetchQuery( loadProvidersQuery(input.scope, null, input.serverAPI, input.serverSDK, input.protocol), ), - () => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverAPI.path)), + () => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverSDK, input.protocol)), () => input.queryClient .fetchQuery(loadProjectsQuery(input.scope, input.serverAPI.project)) @@ -289,17 +285,26 @@ export const loadCommands = ( agent: command.agent, model: providerID && id ? { providerID, id } : undefined, subtask: command.subtask, - source: command.source === "skill" ? undefined : command.source, + // source: command.source === "skill" ? undefined : command.source, } }) } return api.list({ location: { directory } }).then((result) => result.data) }) -export const loadPathQuery = (scope: ServerScope, directory: string | null, api: PathApi) => +export const loadPathQuery = ( + scope: ServerScope, + directory: string | null, + sdk: OpencodeClient, + protocol?: Promise, +) => queryOptions({ queryKey: [scope, directory, "path"], - queryFn: () => retry(() => api.get(directory ? { location: { directory } } : undefined)), + queryFn: async () => { + if ((await protocol) !== "v1") + return { state: "", config: "", worktree: "", directory: directory ?? "", home: "" } + return retry(() => sdk.path.get({ directory: directory ?? undefined }).then((result) => result.data!)) + }, }) export const loadReferencesQuery = ( @@ -328,7 +333,6 @@ export async function bootstrapDirectory(input: { readonly agent: AgentListApi readonly command: CommandListApi readonly mcp: McpApi - readonly path: PathApi readonly permission: PermissionApi readonly project: ProjectApi readonly question: QuestionApi @@ -408,19 +412,20 @@ export async function bootstrapDirectory(input: { !seededPath && (() => input.queryClient - .ensureQueryData(loadPathQuery(input.scope, input.directory, input.api.path)) + .ensureQueryData(loadPathQuery(input.scope, input.directory, input.sdk, input.protocol)) .then((data) => { const next = projectID(data.directory ?? input.directory, input.global.project) if (next) input.setStore("project", next) })), () => - retry(() => - input.api.vcs.get({ location: { directory: input.directory } }).then((result) => { - const next = { branch: result.data.branch, default_branch: result.data.defaultBranch } + retry(async () => { + if ((await input.protocol) !== "v1") return + return input.sdk.vcs.get().then((result) => { + const next = { branch: result.data?.branch, default_branch: result.data?.default_branch } input.setStore("vcs", next) if (next) input.vcsCache.setStore("value", next) - }), - ), + }) + }), input.mcp && (() => loadCommands(input.directory, input.api.command, input.sdk, input.protocol).then((commands) => diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index 39ba22c59d..8f203715a6 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -211,19 +211,19 @@ export function applyDirectoryEvent(input: { })) break } - case "session.archived": { - const properties = event.properties as { sessionID: string } - const result = Binary.search(input.store.session, properties.sessionID, (session) => session.id) - if (!result.found) break - const info = input.store.session[result.index] - input.setStore( - "session", - produce((draft) => void draft.splice(result.index, 1)), - ) - cleanupSessionCaches(input.setStore, properties.sessionID) - if (!info?.parentID) input.setStore("sessionTotal", (value) => Math.max(0, value - 1)) - break - } + // case "session.archived": { + // const properties = event.properties as { sessionID: string } + // const result = Binary.search(input.store.session, properties.sessionID, (session) => session.id) + // if (!result.found) break + // const info = input.store.session[result.index] + // input.setStore( + // "session", + // produce((draft) => void draft.splice(result.index, 1)), + // ) + // cleanupSessionCaches(input.setStore, properties.sessionID) + // if (!info?.parentID) input.setStore("sessionTotal", (value) => Math.max(0, value - 1)) + // break + // } case "session.moved": { const properties = event.properties as { sessionID: string diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index d086582035..6235f35c45 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -570,16 +570,22 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( continue } - void serverSdk() - .api.project.update({ projectID: project.id, icon: { color } }) - .then((result) => - serverSync().set("project", (items) => - items.map((item) => (item.id === result.id ? normalizeProjectInfo(result) : item)), - ), - ) - .catch(() => { - if (colorRequested.get(worktree) === color) colorRequested.delete(worktree) - }) + const projectID = project.id + void (async () => { + const sdk = serverSdk() + if ((await sdk.protocol) !== "v1") return + return sdk.client.project + .update({ projectID, directory: worktree, icon: { color } }) + .then((response) => response.data) + .then((result) => { + if (!result) return + serverSync().set("project", (items) => + items.map((item) => (item.id === result.id ? normalizeProjectInfo(result) : item)), + ) + }) + })().catch(() => { + if (colorRequested.get(worktree) === color) colorRequested.delete(worktree) + }) } }) diff --git a/packages/app/src/context/server-session-v2-reducer.test.ts b/packages/app/src/context/server-session-v2-reducer.test.ts index a1db63b121..00cc37cf52 100644 --- a/packages/app/src/context/server-session-v2-reducer.test.ts +++ b/packages/app/src/context/server-session-v2-reducer.test.ts @@ -114,7 +114,7 @@ describe("v2 session reducer", () => { sessionID: "ses_1", assistantMessageID: "msg_assistant", callID: "call_1", - structured: {}, + metadata: {}, content: [{ type: "text", text: "done" }], executed: true, }, diff --git a/packages/app/src/context/server-session-v2-reducer.ts b/packages/app/src/context/server-session-v2-reducer.ts index 3b9719c098..b34ab3985e 100644 --- a/packages/app/src/context/server-session-v2-reducer.ts +++ b/packages/app/src/context/server-session-v2-reducer.ts @@ -269,13 +269,18 @@ export function createV2SessionReducer() { ...tool, executed: event.data.executed, providerState: event.data.state, - state: { status: "running", input: event.data.input, structured: {}, content: [] }, + // structured: {}, content: [] + state: { status: "running", input: event.data.input, metadata: {} }, time: { ...tool.time, ran: event.created }, })) case "session.tool.progress": return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => tool.state.status === "running" - ? { ...tool, state: { ...tool.state, structured: event.data.structured, content: event.data.content } } + ? { + ...tool, + // state: { ...tool.state, structured: event.data.structured, content: event.data.content }, + state: { ...tool.state, metadata: event.data.metadata }, + } : tool, ) case "session.tool.success": @@ -288,9 +293,10 @@ export function createV2SessionReducer() { state: { status: "completed", input: tool.state.input, - structured: event.data.structured, + // structured: event.data.structured, + metadata: event.data.metadata, content: event.data.content, - result: event.data.result, + // result: event.data.result, }, time: { ...tool.time, completed: event.created }, } @@ -305,10 +311,11 @@ export function createV2SessionReducer() { state: { status: "error", input: typeof tool.state.input === "string" ? {} : tool.state.input, - structured: tool.state.status === "running" ? tool.state.structured : {}, - content: tool.state.status === "running" ? tool.state.content : [], + // structured: tool.state.status === "running" ? tool.state.structured : {}, + metadata: event.data.metadata ?? (tool.state.status === "running" ? tool.state.metadata : {}), + content: event.data.content, error: event.data.error, - result: event.data.result, + // result: event.data.result, }, time: { ...tool.time, completed: event.created }, } diff --git a/packages/app/src/context/server-session.test.ts b/packages/app/src/context/server-session.test.ts index 1e1046fb76..e9595e1376 100644 --- a/packages/app/src/context/server-session.test.ts +++ b/packages/app/src/context/server-session.test.ts @@ -1,8 +1,11 @@ import { describe, expect, test } from "bun:test" import type { retry } from "@opencode-ai/core/util/retry" -import type { MessageApi, OpenCodeEvent, SessionApi } from "@opencode-ai/client/promise" +import type { OpenCodeEvent, SessionApi } from "@opencode-ai/client/promise" import type { Message, OpencodeClient, Part, Session } from "@opencode-ai/sdk/v2/client" import { createServerSession } from "./server-session" +import type { ServerApi } from "@/utils/server" + +type MessageApi = ServerApi["message"] const session = (id: string, parentID?: string): Session => ({ id, diff --git a/packages/app/src/context/server-session.ts b/packages/app/src/context/server-session.ts index e8f91cda3f..69bec61ba2 100644 --- a/packages/app/src/context/server-session.ts +++ b/packages/app/src/context/server-session.ts @@ -1,6 +1,6 @@ import { Binary } from "@opencode-ai/core/util/binary" import { retry } from "@opencode-ai/core/util/retry" -import type { MessageApi, OpenCodeEvent, SessionApi, SessionMessageInfo } from "@opencode-ai/client/promise" +import type { OpenCodeEvent, SessionApi, SessionMessageInfo } from "@opencode-ai/client/promise" import type { Message, OpencodeClient, @@ -21,6 +21,9 @@ import { normalizeSessionInfo } from "@/utils/session" import { normalizeSessionMessages } from "@/utils/session-message" import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache" import { createV2SessionReducer, type V2SessionReduction } from "./server-session-v2-reducer" +import type { ServerApi } from "@/utils/server" + +type MessageApi = ServerApi["message"] const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) const cmpMessage = (a: Message, b: Message) => a.time.created - b.time.created || cmp(a.id, b.id) @@ -954,10 +957,10 @@ export function createServerSession( }) if (event.type === "session.usage.updated" && info) remember({ ...info, cost: event.data.cost, tokens: event.data.tokens }) - if (event.type === "session.archived") { - if (info) remember({ ...info, time: { ...info.time, archived: event.created, updated: event.created } }) - evict([sessionID]) - } + // if (event.type === "session.archived") { + // if (info) remember({ ...info, time: { ...info.time, archived: event.created, updated: event.created } }) + // evict([sessionID]) + // } if (event.type === "session.execution.started") setData("session_status", sessionID, { type: "busy" }) if ( event.type === "session.execution.succeeded" || diff --git a/packages/app/src/context/server-sync.test.ts b/packages/app/src/context/server-sync.test.ts index 9f625c9432..7821a09e20 100644 --- a/packages/app/src/context/server-sync.test.ts +++ b/packages/app/src/context/server-sync.test.ts @@ -1,7 +1,6 @@ import { describe, expect, test } from "bun:test" import type { OpencodeClient } from "@opencode-ai/sdk/v2/client" import type { - McpApi, McpListInput, McpResourceCatalogInput, SessionApi, @@ -14,6 +13,9 @@ import { estimateRootSessionTotal, loadRootSessions } from "./global-sync/sessio import { loadActiveSessionsQuery, loadMcpQuery, loadMcpResourcesQuery, seedActiveSessionStatuses } from "./server-sync" import { ServerScope } from "@/utils/server-scope" import { createServerSession } from "./server-session" +import type { ServerApi } from "@/utils/server" + +type McpApi = ServerApi["mcp"] describe("MCP queries", () => { test("loads current servers for the requested location", async () => { diff --git a/packages/app/src/context/server-sync.tsx b/packages/app/src/context/server-sync.tsx index 109a7bf7d6..6fe6d4bd09 100644 --- a/packages/app/src/context/server-sync.tsx +++ b/packages/app/src/context/server-sync.tsx @@ -188,7 +188,8 @@ function makeQueryOptionsApi( projects: () => loadProjectsQuery(scope, serverAPI.project), providers: (directory: PathKey | null) => loadProvidersQuery(scope, directory, serverAPI, directory ? sdkFor(directory) : serverSDK(), protocol), - path: (directory: PathKey | null) => loadPathQuery(scope, directory, serverAPI.path), + path: (directory: PathKey | null) => + loadPathQuery(scope, directory, directory ? sdkFor(directory) : serverSDK(), protocol), agents: (directory: PathKey) => loadAgentsQuery(scope, directory, serverAPI.agent, sdkFor(directory), protocol), references: (directory: PathKey) => loadReferencesQuery(scope, directory, serverAPI.reference, sdkFor(directory), protocol), @@ -581,7 +582,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { children.mark(key) if ( event.current?.type === "session.moved" || - event.current?.type === "session.archived" || + // event.current?.type === "session.archived" || event.current?.type === "session.forked" || eventType === "command.updated" || eventType === "config.updated" || diff --git a/packages/app/src/pages/home/home-sessions-controller.tsx b/packages/app/src/pages/home/home-sessions-controller.tsx index 06d86c30c9..a12e9800e9 100644 --- a/packages/app/src/pages/home/home-sessions-controller.tsx +++ b/packages/app/src/pages/home/home-sessions-controller.tsx @@ -211,10 +211,16 @@ export function createHomeSessionsController(home: HomeController) { const ctx = home.server.focusedContext() if (!conn || !ctx) return const [, setStore] = ctx.sync.child(session.directory) + if ((await ctx.sdk.protocol) !== "v1") return await archiveHomeSession({ server: ServerConnection.key(conn), session, - archive: (sessionID) => ctx.sdk.api.session.archive({ sessionID, directory: session.directory }), + archive: (sessionID) => + ctx.sdk.client.session.update({ + sessionID, + directory: session.directory, + time: { archived: Date.now() }, + }), remove: () => setStore( produce((draft) => { diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 96ed022373..b348b44d8a 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -872,12 +872,17 @@ export default function LegacyLayout(props: ParentProps) { } async function archiveSession(session: Session) { + if ((await serverSDK().protocol) !== "v1") return const [store, setStore] = serverSync().child(session.directory) const sessions = store.session ?? [] const index = sessions.findIndex((s) => s.id === session.id) const nextSession = sessions[index + 1] ?? sessions[index - 1] - await serverSDK().api.session.archive({ sessionID: session.id, directory: session.directory }) + await serverSDK().client.session.update({ + sessionID: session.id, + directory: session.directory, + time: { archived: Date.now() }, + }) setStore( produce((draft) => { const match = Binary.search(draft.session, session.id, (s) => s.id) @@ -1296,7 +1301,13 @@ export default function LegacyLayout(props: ParentProps) { const name = next === getFilename(project.worktree) ? "" : next if (project.id && project.id !== "global") { - const result = await serverSDK().api.project.update({ projectID: project.id, name }) + const sdk = serverSDK() + if ((await sdk.protocol) !== "v1") return + const result = await sdk.client.project + .update({ projectID: project.id, directory: project.worktree, name }) + .then((response) => response.data) + if (!result) return + // const result = await serverSDK().api.project.update({ projectID: project.id, name }) serverSync().set("project", (items) => items.map((item) => (item.id === result.id ? normalizeProjectInfo(result) : item)), ) @@ -1479,15 +1490,20 @@ export default function LegacyLayout(props: ParentProps) { return } - await Promise.all( - sessions - .filter((session) => session.time.archived === undefined) - .map((session) => - serverSDK() - .api.session.archive({ sessionID: session.id, directory: session.directory }) - .catch(() => undefined), - ), - ) + if ((await serverSDK().protocol) === "v1") + await Promise.all( + sessions + .filter((session) => session.time.archived === undefined) + .map((session) => + serverSDK() + .client.session.update({ + sessionID: session.id, + directory: session.directory, + time: { archived: Date.now() }, + }) + .catch(() => undefined), + ), + ) setBusy(directory, false) dismiss() diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx index 48b432667e..dc9153536c 100644 --- a/packages/app/src/pages/session/timeline/message-timeline.tsx +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -812,13 +812,14 @@ export function MessageTimeline(props: { const archiveSession = async (sessionID: string) => { const session = sync().session.get(sessionID) if (!session) return + if ((await sdk().protocol) !== "v1") return const sessions = sync().data.session ?? [] const index = sessions.findIndex((s) => s.id === sessionID) const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) await sdk() - .api.session.archive({ sessionID }) + .client.session.update({ sessionID, directory: sdk().directory, time: { archived: Date.now() } }) .then(() => { sync().set( produce((draft) => { diff --git a/packages/app/src/utils/server-compat.test.ts b/packages/app/src/utils/server-compat.test.ts index 605fdaace8..52e5ec6e3b 100644 --- a/packages/app/src/utils/server-compat.test.ts +++ b/packages/app/src/utils/server-compat.test.ts @@ -53,6 +53,7 @@ function setup( } describe("createCompatibleApi", () => { + /* test("routes V1 archive through the legacy session update", async () => { const { api, requests } = setup("v1") await api.session.archive({ sessionID: "ses_1", directory: "/repo" }) @@ -63,6 +64,7 @@ describe("createCompatibleApi", () => { expect(requests[0]!.method).toBe("PATCH") expect(await requests[0]!.json()).toMatchObject({ time: { archived: expect.any(Number) } }) }) + */ test("converts current prompts to the V1 prompt contract", async () => { const { api, requests } = setup("v1") @@ -127,14 +129,6 @@ describe("createCompatibleApi", () => { ]) }) - test("keeps V2 session actions on the current API", async () => { - const { api, requests } = setup("v2") - await api.session.archive({ sessionID: "ses_1" }) - - expect(new URL(requests[0]!.url).pathname).toBe("/api/session/ses_1/archive") - expect(requests[0]!.method).toBe("POST") - }) - test("resolves protocol detection once across implementation methods", async () => { let detections = 0 const resolved = Promise.resolve<"v1" | "v2">("v2") @@ -147,12 +141,22 @@ describe("createCompatibleApi", () => { }) const { api } = setup(protocol) - await api.session.archive({ sessionID: "ses_1" }) + await api.session.list() await api.session.list() expect(detections).toBe(1) }) + /* + test("keeps V2 session actions on the current API", async () => { + const { api, requests } = setup("v2") + await api.session.archive({ sessionID: "ses_1" }) + + expect(new URL(requests[0]!.url).pathname).toBe("/api/session/ses_1/archive") + expect(requests[0]!.method).toBe("POST") + }) + */ + test("uses the global V1 session search endpoint", async () => { const { api, requests } = setup("v1") await api.session.list({ parentID: null, search: "session", limit: 50 }) @@ -160,6 +164,7 @@ describe("createCompatibleApi", () => { expect(new URL(requests[0]!.url).pathname).toBe("/experimental/session") }) + /* test("projects the V1 default branch", async () => { const { api } = setup("v1", { vcs: { branch: "feature", default_branch: "dev" } }) @@ -167,6 +172,7 @@ describe("createCompatibleApi", () => { data: { branch: "feature", defaultBranch: "dev" }, }) }) + */ test("translates current file searches to the V1 dirs parameter", async () => { const { api, requests } = setup("v1") diff --git a/packages/app/src/utils/server-compat.ts b/packages/app/src/utils/server-compat.ts index 94374aa9f9..1df1338b71 100644 --- a/packages/app/src/utils/server-compat.ts +++ b/packages/app/src/utils/server-compat.ts @@ -27,7 +27,7 @@ type CompatibleSessionApi = Omit< shell: (input: SessionShellInput & LegacyPrompt) => Promise compact: (input: SessionCompactInput & { model?: LegacyPrompt["model"] }) => Promise rename: (input: Parameters[0] & LegacyLocation) => ReturnType - archive: (input: Parameters[0] & LegacyLocation) => ReturnType + // archive: (input: Parameters[0] & LegacyLocation) => ReturnType remove: (input: Parameters[0] & LegacyLocation) => ReturnType } type CompatiblePermissionApi = Omit & { @@ -183,9 +183,9 @@ function createV1Api(input: CompatibleInput): CompatibleApi { async rename(value: Parameters[0] & LegacyLocation) { await legacy(value).session.update({ sessionID: value.sessionID, title: value.title }) }, - async archive(value: Parameters[0] & LegacyLocation) { - await legacy(value).session.update({ sessionID: value.sessionID, time: { archived: Date.now() } }) - }, + // async archive(value: Parameters[0] & LegacyLocation) { + // await legacy(value).session.update({ sessionID: value.sessionID, time: { archived: Date.now() } }) + // }, async remove(value: Parameters[0] & LegacyLocation) { await legacy(value).session.delete(value) }, @@ -308,34 +308,34 @@ function createV1Api(input: CompatibleInput): CompatibleApi { if (!result.data) throw new Error("Project not found") return { id: result.data.id, directory: result.data.worktree } satisfies ProjectCurrent }, - async update(value: Parameters[0]) { - const project = (await legacy().project.list()).data?.find((item) => item.id === value.projectID) - const result = await legacy({ directory: project?.worktree }).project.update({ - ...value, - directory: project?.worktree, - }) - if (!result.data) throw new Error(`Project not found: ${value.projectID}`) - return result.data as Project - }, + // async update(value: Parameters[0]) { + // const project = (await legacy().project.list()).data?.find((item) => item.id === value.projectID) + // const result = await legacy({ directory: project?.worktree }).project.update({ + // ...value, + // directory: project?.worktree, + // }) + // if (!result.data) throw new Error(`Project not found: ${value.projectID}`) + // return result.data as Project + // }, async directories(value: Parameters[0]) { const result = await legacy(value.location).worktree.list() return (result.data ?? []).map((item) => ({ directory: item })) }, }, - path: { - ...input.current.path, - async get(value?: Parameters[0]) { - const result = await legacy(value?.location).path.get() - if (!result.data) throw new Error("Path unavailable") - return result.data - }, - }, + // path: { + // ...input.current.path, + // async get(value?: Parameters[0]) { + // const result = await legacy(value?.location).path.get() + // if (!result.data) throw new Error("Path unavailable") + // return result.data + // }, + // }, vcs: { ...input.current.vcs, - async get(value?: Parameters[0]) { - const result = await legacy(value?.location).vcs.get() - return located({ branch: result.data?.branch, defaultBranch: result.data?.default_branch }, value?.location) - }, + // async get(value?: Parameters[0]) { + // const result = await legacy(value?.location).vcs.get() + // return located({ branch: result.data?.branch, defaultBranch: result.data?.default_branch }, value?.location) + // }, async status(value?: Parameters[0]) { const result = await legacy(value?.location).vcs.status() return located(result.data ?? [], value?.location) @@ -451,9 +451,9 @@ function createV1Api(input: CompatibleInput): CompatibleApi { }, pty: { ...input.current.pty, - async shells(value?: Parameters[0]) { - return located((await legacy(value?.location).pty.shells()).data ?? [], value?.location) - }, + // async shells(value?: Parameters[0]) { + // return located((await legacy(value?.location).pty.shells()).data ?? [], value?.location) + // }, async list(value?: Parameters[0]) { return located((await legacy(value?.location).pty.list()).data ?? [], value?.location) }, @@ -485,11 +485,11 @@ function createV1Api(input: CompatibleInput): CompatibleApi { async remove(value: Parameters[0]) { await legacy(value.location).pty.remove({ ptyID: value.ptyID }) }, - async connectToken(value: Parameters[0]) { - const result = await legacy(value.location).pty.connectToken({ ptyID: value.ptyID }) - if (!result.data) throw new Error(`Failed to connect terminal: ${value.ptyID}`) - return located(result.data, value.location) - }, + // async connectToken(value: Parameters[0]) { + // const result = await legacy(value.location).pty.connectToken({ ptyID: value.ptyID }) + // if (!result.data) throw new Error(`Failed to connect terminal: ${value.ptyID}`) + // return located(result.data, value.location) + // }, }, permission: { ...input.current.permission, diff --git a/packages/app/src/utils/session-message.test.ts b/packages/app/src/utils/session-message.test.ts index d998c0c045..a69c414e16 100644 --- a/packages/app/src/utils/session-message.test.ts +++ b/packages/app/src/utils/session-message.test.ts @@ -49,7 +49,7 @@ describe("normalizeSessionMessages", () => { state: { status: "completed", input: { filePath: "note.txt" }, - structured: { title: "note.txt" }, + metadata: { title: "note.txt" }, content: [{ type: "text", text: "hello" }], }, time: { created: 5, ran: 6, completed: 7 }, @@ -170,7 +170,7 @@ describe("normalizeSessionMessages", () => { status: "completed", input: { path: "/repo/README.md", oldString: "old", newString: "new" }, content: [{ type: "text", text: "Edited file successfully" }], - structured: { + metadata: { files: [ { file: "README.md", diff --git a/packages/app/src/utils/session-message.ts b/packages/app/src/utils/session-message.ts index c67c6c717c..93d86a66bb 100644 --- a/packages/app/src/utils/session-message.ts +++ b/packages/app/src/utils/session-message.ts @@ -304,7 +304,8 @@ function toolPart(sessionID: string, messageID: string, tool: SessionMessageAssi return { status: "running" as const, input: normalizeToolInput(tool.name, tool.state.input), - metadata: normalizeToolMetadata(tool.name, tool.state.structured), + // metadata: normalizeToolMetadata(tool.name, tool.state.structured), + metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}), time: { start }, } } @@ -313,7 +314,8 @@ function toolPart(sessionID: string, messageID: string, tool: SessionMessageAssi status: "error" as const, input: normalizeToolInput(tool.name, tool.state.input), error: tool.state.error.message, - metadata: normalizeToolMetadata(tool.name, tool.state.structured), + // metadata: normalizeToolMetadata(tool.name, tool.state.structured), + metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}), time: { start, end: tool.time.completed ?? start }, } } @@ -337,7 +339,8 @@ function toolPart(sessionID: string, messageID: string, tool: SessionMessageAssi input: normalizeToolInput(tool.name, tool.state.input), output: tool.state.content.flatMap((item) => (item.type === "text" ? [item.text] : [])).join("\n"), title: tool.name, - metadata: normalizeToolMetadata(tool.name, tool.state.structured), + // metadata: normalizeToolMetadata(tool.name, tool.state.structured), + metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}), time: { start, end: tool.time.completed ?? start }, attachments: attachments.length ? attachments : undefined, } diff --git a/packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz b/packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..bc1b664f82dcc55a95fc45b4c3b2c5b0cafaec55 GIT binary patch literal 67724 zcmYJaQl@s%1CNf6+tu< z(Ekh&z)kOu`?h!zCAYwN{lVf~Cqw6a8hAnq?IunAF{gVI&*b~2)ay?2ak3yODG>-T z0+6(l`AdKQ6$I2#u|O3LkeJkIZY*VEe|1+?7k~QR%~bb41KOwjul{n!=kz=P9xg7f zJUPiL>Mxr=KBvi>p$lUkK0fE%;o+bun7<>!PJax-Z&Rp$HR}_KrZi%9jqP3F_t!dR z@#3Dr@@8rOzgKy2?{uv;Sq1$bLyv}h0N3o%$bNisGqaj283VADSX`zDSQE2i{oom=E^bB0Y4(0J!&p{P*GH+9nW<$({7%=2v>pEBUR( zG_2n^>e35s=`@bnq?;QmWLWvm4uF5Ex<@4Dy|Z*!3U$?A0WHAeQ{W6k>e?1ROJ@>F0=1+j8xPb9pRx8#jMl zV{GT$hql8hIt%lx#MJ1S@w?p_?D#%lep)=&54>R#?DzkieZ74}e*igKUI09p`Tj2W zuR9_{{MsMy_s4>jd_^@I$USZ%2CI032Am{ zbw@#h-=S|CnNWlEE$QRJUf}oL$R%;4+RRTf{~HW@0D1PvGVR@A#a_UtUBEavuQtd5 z!QX&3FqpJ>7s*DVKO%BUs)yO&Sq z*dg_Upy%ds(=t|TUYhvZCdFj!f?lGCkIQ!F>j<_lh>iIB>w^7XAj;3ZQ^QZF`>Fp& z{bFYNrvRDJX{X}U>My~bF*L}}#a%T+)E!|aIwr?#y-*jTCJ^TRv@RB0v{-&j6{ZQF z232nU%4?Q??^{sfJ(|Pb{ufM{sqnG+(OQd)9~z_OL;8K6C;Jq@O&NFDYi$Mq`rG!# zApmF6hRIHy^Nks={(D^XlBMm`>1F-6ALx9+e$pT5e3J0p9_!C#4I}^FyJgOdk?30EDlQ)s)aG){FCP=#?S)k0Pt4;Jf?s30FH|vQtkwH z{Xg4Rzw$x*R%&dof8+JaCjo~XU4ZR(4PMd>9n^?{HwSAGc~Jfn?RYRhDu!47;h0vx z*HujL%L6??dlU2mq<}_zPq4c!mcc+p{r)g0iLrLlvM|*Q8s0aax}|PX@)fmyzksir zeVbu4AJzH{P69|r`4`{|&>`*DKR!>hg@8ebpr>q0IFSyKu$ zc2j2D{W`z-%Zu4B1QHZn)zsp1@e-U)zpH+~!m$e2CDpV#`bb%Tdlevmv^Krt`2vif1nV5QGF3#`CPj(gz!zbkN5_Qhg;$t~E z7Kz`DR^$hNUVuC~*0>$pK3>j^zm~3ni)HG@y+SxBf=a)eAO!|KN|}?7%sbcvRBy_F zXIjApf#v;pu6P#p5H�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%0ADAsYp?N1>Lhg`3sh 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

7Y+QD}T8_DVKLjGZ(7vq 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 literal 0 HcmV?d00001 diff --git a/packages/app/vendor/opencode-ai-client-1.17.13.tgz b/packages/app/vendor/opencode-ai-client-1.17.13.tgz deleted file mode 100644 index 5939f2cb39c27da205f1f37e9971009a669b83c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75585 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 z 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

$=Nes$vsW~pOCtUnEt6JQ zB;j*;bp=-#ovJS6l6GN&5ww-043cuq{uCaXq|oM))D*SLbp2E#d`gL656~4Jo*?1o zp-!QM(HI^nwyo^h(cCvq{bIL;9@{C-35P&C%P=@^v^0zNz=nZ;ir80nqHs`_jDb_C z25{kdL=s7W2tWzm4MDEAT09>b721I=X$?%GwJtUfe@yA-VzFLtXwo3Byw+Uwdz;KV z7sdVQ{-Go<8;9Pz-B0i)VXwF)`P&^Ve|l9|b4au2UlCr)BuO3Nvgq0hK@Ll17tbTk z@)(DRjwguO7eR^Iq(&nPdEDk}=5w@*K3Jnp8K=9dnl&0o`k>VuwY~skgA!k#i)dFG zU}BJcyOVXamj!4EvQS=^r?;gJ#agIOay6*p)1l4ZNDyMqfP6P7%6`H|ac{}~y#X3! zzXi_komQEj9bF@1(n%*ksO6=e74o+sjJ(J`xSz7O$p>dO##zb0u(P!n7mYP$S07Iz zC5HZ)N)XHvQC3JR^goj8p^a-^-~bI?4>O#;HoWKKIiB^7hRsFY@-%`Gs|=t!lrpa0iNM?gpMgK@u+{_jC-G6$Q%{C; z2Jz8^bE_h6gi2TsZH%uHHwdBpxV=~gLCq0h4!1!kk>!II!2L+vP5ycAh6oei_eKjQ z#g9PxZ;h70=!R28Y64)zsffcsP=E3>j}jr@HjR%yL`Hi4V|>M_qc2+7l+P&&vnOOM za*u}F0iMe=Cup)?yrl9M_*0B%uWw*hiD89tuGgB2Cv#GwT;qXL^(liSGHT{ZIor4R z9CcttC&4nSDQ7I6ec^w!&)K0JI2(fp`=;4C;effTc|{X%734J1AkeFU=YeG75a%W_ zu<$vYLHDqqU=%W2o(hJWY*b@^SXe zsAy9gVfEkkqMPEReZ3SRusYz=P+H+~U>HFlM4^M>QQ29TAr%mAMbOjtwcbZz#2lVE za3hSTIOq`!?rTMkK-}dqCW47zbtisrX@sHd^L=3@B6yzv#_(bp7&lNaPi~}S=q#EI zp*#f3dP9hXXKa44Pqmb>t(%Y4QCQ2FW1ze&(kPYMz7XRc(Hk#GG63Tm^+C9GAxf7a zDRuS_lY$H%us74ju%!v?h=H%t1BXd+ROKc^GdGTfnCzC=BnQ>hreYkFhofc&jRfe{ z7oXz!ev9jR7S)@0yrl4kfx`MjQr_m_aQ#E~Y;W~CMTvJX`>h+F(6(BRcb42G)pzjk z2z7^qyF4QXMbf* zn199?L6X!fCK6Y>1ZgUan<^H=V8<3(1%DwnC7yCBDn5#{y10GeS2xtu_n~fq)|Cuu zCHPsXWi)FQ}H zCq`rR52F-p?VN-tCk%Od_I!7n^5dE31rlWCAif+PEYUqn7|P)PG;0}SQ01wD2{_APiNEQUp9p>{5D7mKT(;iJvcmw@|_rQ z>6C;JY^f__AAk4*J?^?Kpz`-t+{5yV6Pag=pcg&t7jp}E39Tv|5?huz$Fm(YDT8_e z77H+bG2DlW44D%;FIcN8EZm{p%V8SIwMgw)Y7c+xew-V)Z8WT9JceqibQbw60jIv2 zn-RD4&F}rjzd|G~sxnI;Vw?V%FYnY$-!E0^8uxAJkFC^Xe6Get(2SouUv$#j0?0yU zTVD`?AlPJ9A0ZfkzyUDxEcbL7wm=%e!BEB2OK(N3v)>2k+Vu4;IdU{}`%SZJ%d}n; z#ve?o@d~RmmIE7|f_n0MOHsB-rPDTq8Xb$0nBQ;j*s#r*R8bdlbr8J5`uUKXMsLtA z2y;*ES5CzawAvuP7=yB|L3wjBcoJb(jCXZq1_wz| z^KmOFSWP&r6IY#gwWJ*BcGarN)?O@idKO%oEI(f$>Ek}vS69b8Z=OFz*u{As0h|)K zC$|9N`rY?jgd<>Rpt-@Li=Uq#E78l@?s6*Z`%i+~oAHl_N-zGLJOboMDys3r9T#VQ z9)T}~o6gudhxv#o5E|Smo=q8uLRiv#1Ce9trdW6}K0n&NiyJYYuL|I2;MeT;=JX5J zs;|HQ@^d`}v#9^`B#;ycc7G!@A@ueM9OQVt=(*X!BDRSK=6<{zrIP`xzCHuVSA0Kg zx%o8{(D_ZQ9JcK6sh(c~EvqT6Oy?p0OiOhDd-y)D-wXd@AAbM`uL8{s_6@LToC+N^&4cH$7uD8&_I6`T1JmO| z_3m~`ZBlG6m+q>+ZEiEeRqfar)!GTWq-u?_!9AMSL|^Sv06(s^aj5AjfAraWDSK7c zai0aX!eQ(V=+F79&?y3Fm+}wQjL)Za(_k6YFTfpdER<6Tf8UQEF@@Pff~yc-YkNf8Avv@gO>F1a(2?_?ZFXu*z0}dzn4MgS>`P(BY%0 z@;P3V^Ix}(n_tSdLiP3)rc^owwKhzBLwz2y!YFFWDGw38n$5a5?@vmf$Po1PW^kwb zyg<1e`?dSL!2K_RAGvoixpb(Q#?f&Mm#n`rGnjC)v)! zPlgq&oQf8;g;OK^>G#h%JlK*kQ?KfbaNq7J3%R@;g?#N>9dN&rcEXf1s^yf0-~vo$ z%KlUX^x1$uNy3D;2Tk^tc!caS$r;f{!qXaXPt}yQcVU!{?)Mh{EZgF{X_WK&X8S@HR%pJc7GrPDxIu5A8xW3gd|lKD*0rWto*m~ zF}>2T@#ZlcvVU=z?_w1a38}v79VHk3oxKRV41fvKN|C2jK#AF#KrrFmV-m+4L_NvT zZy*dx^2dNdQEoqhwt%x0{{e^X&8my}-YF0BeFha~LfT8iJQ^$C}S|8QY zyT+x^WK1D(*gNsvJ<60sc1b0VZ{tZlk>+V3Bk?K+6%`L7z>PPosHzxAXNZt~{q;Gu zZX@Eto>B4S06ub0kKhCjLy8(GpMy9ljvgg%C}8E&YGad9!^AO|YJ zD6%PleTsA1m+H3Rue=f;Egyn%oC+?X+lz3`2T^Q?#yXh&l3cSWn)j3u9*KHYH;8bQ zqh};BGRPyfX6=M-D~F9LsNq|EM!FA|RQHd~6jlIt=apx=k%Z58YR!lZ(PFEJorreP zKef+io+VNYAwBWBOCsDhbt}1EF_xy%77mSt_S3So*Q*>3;^K|pxy(Ds7|@8YldWi_ z^XKUc(~KJoC4TM@#H2mDGSr-%+R@)G*>WE!jqu&j@xlbwA965AZbTIygQqWQDliP_ zjs(pWQ4KqTo9QzZd6>4i(p+4D_h!&@mcG6~bde^U0r53_E0eRz7nbUI*L&PjMf+Y2 znrDaK5A?$MO9K?!>1KNb9hVa4j8G+2ZN-Y>ePn81_%mV zmjXm0m8tLidH%ifsfgI)FRYEIijku}#2pG>u7?Fsj!Ln`()&h(S$5~MIis$VEw`}C*PFA@%DAA4Am}(!;)!nSJJv3+XIV#7L z!3|BiX%He|M2ie}6P??~@hzIKOux$OC}xuhE|*+nkAhag2sBCA3$U5laTJ#yioMDf zF+VBk{~Hkb3EwNocJQPVzK)jYW_e4czRj9`j4~8>g(@i)%T~c%4W>9{LXwvBhd0~^ zAU4=5)sdB;!bYKZ8gVN_tTj1ni!ygQwyG&frJAUqn+kL9qo<97DR!_IOOXm*PQHOG zkI}ukpic0FwcjPove{8LXtTl`O_vgu`%$f*snkk2P2DehlA=EzSX46{FcnsCzHY#f zt75gP-_B}i-lM^H5$;2jY^Fx7`q{ib>$Kt}veB&1R7}7c>lYJEx4~}aLQQx0$4^dy zQirGn7G9g^A`3a2h!{r`N5hC!!=m}sznrXx1WOW=pjEICzKi+2Dmd#dsUyt!>1d-G zl7L9gR`}I*s;LyBmYb#=zi_@Rq}j53LkAO_^j=U^L^4>sBBi=~f&eDexVwQ&w$>Yk zRzfy~36oa^o`qUYLcC0YBU3c>1)jT8l^hD8S~Z&E9e0hFML0s4=|>e@(57!;^YB7( z0!ayt_GJWEv{YPetJI?%^y%>h*)I%5`M2+dGg#QC(cNS4*a{sZdQ$YmN%6~Ic+L0y zdYH^O?_b?#9=LlVhdJ*z3HX}LQ|HKBU5`vme*THnRlJjfqWMrRzGdAflEpC82wnCk z)xlK1=Sqtc;J7L!GpmCBrD#w7P?j>KC~v;#O5U6h$@?u!lTO^MTZFafe7M-%uLzuC zWt}%LU*||(2W;u>)%wP#iP_PD>Y8N%VgVX`RqOIUeo*waYvt$EnXoj|^63caexSR~ zt>DeGb|3^P{PESnIPOqQ@V%1{g8L-v^syWooCw@NdzU;ZY0A6%R*8=l%C=i5$RGB# zkg9u0(L$dYFzBBf2_-$z1XHBF&HCdgXbpozeTk*byLBr%@8}^Ju{86{K6wW$H8+up-gsQTR=Gp<(=a{S? zo;9=@W9b3V+ynH3j2C~P3J-8v!kkf{GQ-yw32<_zreFZH&yB~cAPDVuq19yl95&&|IGfelGPGBh#E4T1P*=gNU9c z8Zp2?Zg9OK<4j292eK_tn^(2GRV~VPUmumFio`Ya@*gu~IOpCECMu_z6%%deCvEvK z{{GREbxcZa`%=%RuAn84Fzx~1!(>Vd10s3R@HjV`OQx}fI<2NTt$qa0b|(6_={p)^bJxRL z)n@VRPbs!9Va_OKJA2Y!wxP|1tDn65t3TzC7#PcE-~;o<3@R`n-2){Wg+EJE`N<)f~>1MPE@6Z2YHD|DYETF8CXHnMj+T_T2lKN)!Ss zQkoYpe0(%PX8K^t1yWm{kGVjGGOfs{EQ*>-^%I{bF03f8xIykoU4ul|0KQ?e18L|S z66;33J0*z9Er6~%`MjeqixNEcl!CBs2Md7v$hpQ8FUk@dh(&>TZ^1_>Z+z!hx(pRQ zqXa`mK0fnI#g!kB=@+Nj6SjsqO){``1$-=xVagwGNrY&qiJ8^XYo-G|iW`x6RwsMV zh2rGqq%8pP&~&cHES4M8KI%c07?K{AIXM^#lN6-Xt}*7x1uy?PjE{?lcT4iGbI`^jqR^0rT2r&)X6w}Er# z*=h)0#Sm<=_Sr~7NiXHr!M0*3`%OEeTiR%%H0c~Xmj3Ldz})DI3;F)i_95ZJn{3s2 zX02Y39r4{4i0pgZlJ1GbyoQ=+9xbbs2m64n1q&X%u265*5bHIfRc7@|3)a)#@3V&K zf>n-koalGz$_LLI70J31B0(Par}hI}6Ly}lgbr8}B7lvSDKAl|wkGX-iQQwB!dLCq zX0U^4(EjbCbq8}}3?4|5Cuy`muWSjsWKhrdWueLaF@64dIjuXc^Cf(@h?azXtN+_J zR>#kOq;08NgQ81JP9xi3YjHCJ8n!wND~G* zx8+fAVHqnnbl{2pw|_CF3A3brMQGKH<+TOBqi@5A!Tp zWqL9+N2wxGy8M}-DL~RnE`KlqeDs{Uugw5$tWDfj+w-?&)rK=UZ5)+^R_O`wMSD2& zbK|L$+SuvgZG;d^{)s2PerF#j=LHuntl<@J)}IhH6hYI6t9iAY|3@tuw{8%^s~2l1U~$fR<_tAyr!kGgOSr-jc_nXGEU z9A%D!1;P>OcI?v34sCiy@a)d|!>m(TJf&DxZ=KYjS#Pv2jt>UE`9G7z3$Z2h97xAQdMP=1FVQNXX6T*~Emx7|%JCLmIT~g9MWXof$#s-hXp=_qt2`5;_U|wSAt* z4GjH{+|SQ@dk7>}%1<>&ZS6ZU#5T#0>6$AkbZs`($&UNzxxHu2Y0+x1*$f>H{iWO+ zsP27_2|FIEYb4p43qD4Z3g%THd-d9M2fVW-`dkUPY#Gl7B1;Y@?*e~*1LeR@sl^aPnIR5E3MO|F43<9UxiLa~TSA62yQm&Ob4x z8Q(x7shagke$}bx-?qh{4>UwnpFqE)4pPX!A3{7pssP~4@%L~g0lMcv!gFExPvGR; zH?;n8=lunA5kh(cW>ob10~4hXKY=pN-&D6hzvd^9KZjw6o%W9w1kL{VgR=4wTRzZj z|0&rX$9o)&N)TEBjE2WFW%U5774hS)!;2}w*Dnitw6WkT^-hic-9>NBjPVnB0}gguw&=#$_4-|jO+V!68fL;g~M{7lN7Y>8Bl8c8^`~@ z(r`Y3o)v*~pFnH(tN+R}1OhvW^S%Q4&=x{~LWI7rf&X9UTj1g3!UOO>@#DK)+*hC; z=s+tlbkCd{uaNyQ@V{~I)!zaGnjR}*Zv0BypjV(m=~Z_Gw`(V8^O=e(=o75|Q!$qH z>XVWHYkg$LE557O+GgM0VJv11Uuzpbo44#&iHhKoash(u8vwBS(`h)D);7e?;&2E53Z~7z9v%+WR0;b}LykW%&9}eig%F$hbzX#ey#qf0 zn&(8GHoQbEb7 zmK@|vHNWz}%qoj_WA8X~LWuy-1`tWqO9>dDeL;5#`eS^qgf}<>JFH=l!6fgc#CIDO zBWCNUN(ptv1{WdQW60<*FPquuS$f zQ*a#BmMU3u_4Xb#pnO56wRSK6|rP0o* z0bHE0B2EVbx$H^jcpANC4Un3;d z2(%tE*%#K39BK)wa?<>EVWB%%I=6IvY7+w-%OB9UpAbaTS zkOCZeJIol*IdJQPm5nSsJ;={9Fekv;Z=f)jcB$kJm`xQS2mnTx9VsvDVt`g1oNYt$)3DqkX#6k57?dUp7S21?(gLrp zidg653MO~7c(BHIK4T-KmR#M?;{{Q?h`@i??xB9(zduFc#ey3l77!;x&2NKhE(gTm zTwix)?sjzwQ|uQ_2-aIUm<;Bl?3W#o{SdWjWOGegDNSeS9;tFUP$^P7w-AJ3cbR%&<5kx7*Q%!mN`H$Z zW4hVc%e5O={5Uowvdi(B@c7&;#=+^J;FA52FK$G0f`;Xjk~Q=2dESLZkT+~wV+qP9 zCczF1dCO%535x*534j=RT8*2{M|5I@Y_iq2prr;fZs`lfXq%o z%iWjhdGI=d+ZhW$;U?CW_le*;D*Mr@RJ{sALseayZ{fZHSPh1fJ@Zuy5!<{z*3}Fq z+x$WnY=Z77w0rsJY;(1A>s&xsSYJ2A82H5u=3wNwbrgcE$g;E}1q*~#RZ-S= zGX6|O5Ca2EMR1tfrD2dLia$qMKe*kC7S8z}i-Op(9fVmtK_IigG+4P1#9NJ;QWVU< z@~teLf-;8(Flrq`Xf#pH!fmp1u%W6Hp-Z4?N@Z9=*=HW2n1!M|t5c*~v$)Y6YAvCg zG%_wFN*5m%67sOImY!O5v?~eM!v$P6oY;o)E^Or_$9XERuY9)c_(!kf7J*kd8Wb4E@K!+;Il^N+stF4|Gd$(T?7Bz9%B0&W;hazTC z^J(r!fW&6EU#20>6F)U1#*=m{QgB)ZjbsEL}M0C1zXbcpG+b8`k-tzQL4bV$e;xZ z8JHN5A?bE6fRugk>wnld-iHU}z`|dS!n}@P3(e$Ml7kDr9Q~8#HqgO)O-ELa z1CU#X!aXqyfCW=bgY-0VYze;pEQ9SuPO_zmQrn8Qve!iSYEQ#v79U^Sn{S9M9OPrJ zpt?JIGPb-*F7}+jnoaz(xzcj8Ns6}dszo8^!m(f}gYayA;?69^C8p z`y(j$`GCZu@g0(&n-m1xVaw)LPoGB37N5y?;w8X?qb9b$8c(dR2w*4tQ{*tDTyo+H z@zxcu$V3JbhP@8e`3h}T1P2nywAD!*yf@2oM>_&9k4t0I+GTaHN*zb)GFuE0$135t zW-z;A(HGEkA8%kP3v*~K;8q#zd(w<$nZ7NJHj9%WH8^d9g=FU$be=1M&EXj>L5fiN zG$n$F;|xU%YD-u)-Fd%Q(@&b9=`T!uW*tt>o1LKP=S^}=(R|CBgI{56QF+V*QXDjb z2bwC(R(2wjC0-K!+!=qWW=)^MiP4+SF*1&j0&Qs2TuEtsfRR@WgNSMphLAS|(S9ce zT9_r&*GJ0)ZBT5k8~=Yq)i;;_*oqUQv$(R(O6RAah1wW0%c1T#XSc45t!@>S2x1{2 z^8dJc=itnqsB1K~?U^{4I1}fIZQHi(WMbRq6Wg5FwryLJ8v5O*!3; z{(`NpxH%|k6q*1Apa-fIz3R+RR>`TMq9M6;QzUY^x)hYfWg1rf7R*dkTPNC#x=yE^ zugi1S+%E3S=%|3-$wfZjzezE?} zK|KLS2Q9RFs)@nhwMWDU-mx2dLX{`btr~Tl>VO)=EP}A$D~BVq=0In}9Ya!+u(L+> zhG+b5Z)dy@`~vz)|47y4u=b)PsK4e8B~Z0&{LNc@8OrDyfgn$jrp1iq4}Z^)5hpMv zS(ksas%L|BK|4Ctqqto#YhcG3fn@KD@ciU5xt&fMYfLjupVjv1nt&qao$-QXPsKIt z(n(pJFwXOu-hIaQQ$td`g#SDb zGBZx*Sy64(cBg6Gs9r<8^Y%Ghyga_NXZekws{847$}I zoo)sR;$FQ5FMp@qt0lM6)~@aH2L*ls2pa}Ti^ZiF*{NP6prf3^f1OfNiRn%U+o4LI zC9prmsom4TW6X*=4?(cO0k&c?S*lCnQX90z0of&({ItUY&2ERppI>evR3m?= zx+A^*$B-UMhejOpLc&mE#P0;)ZhYg;8Mv&UFCa{oT zqfkE!fnu+>9KyTPy(e96mFG+W337TO1P?mroOkwduFv28I@jm#9B-gc_1EFJNxoHl zPQ%#FA9LQncNT~R)h~i)qQ=E>f`pBOG}F5S2d#z+el6?7V+2m(_u!b6xGlYN!H&N3 zVHz}xcf|tb$$;zrw|dO2Gd7BG$HUtpAHO>C%xAC26G$9jot!$DY$Zb8yH~8~RlQdZ zb{^ES>gYw)5_yI?J8F%uKG__7yC-BYS$IE5PHOTXlw9SAqBG2;Iz~;V>K`7C`7wYN zJ5{(!B25a%#C&7&5)bn4r-8zH*`)C41z@7!TFI01^}R*!*|+%EuCtx&lJKe9U4hJ= z@P=>NLnVj3(K?J5g1-np4{Z9G%Y7Kk8)*y6r#td^_P)KDgQ9U07|u%iGRs=nuc@ z#v+4%N%B|!BkD(+Yqi8j7Ofi^>8{H^UU&CQgBSNxVn4UgLqlcMfm_~b>+=^y2G3cQ zxw)x9nYphT7$KHZk!a~0(JJgr-zu%)!xwihR z=-Z)_*cP7U!+{k24!J*ZUjEjCX5Kb(=2;XN4SdoU08!1!gED@z-;o^&2D>_oMUh5% zPpIjh{TE(_<}XETtftXAnQp(-q^eu=Oah<5XFz`DI+s=pZbMOY2Cfuccs@!rov#+g zKVtpKr_VCz*mz!YJZbpqKn!|?BJsgb$~9;K*r(^VH4cF3TTsQnUouO#mG32|dHt>L zt_+C>j&*^>JC~m*#X)r20_P6CO`Md9j-BFngnu1yKeH-7F(&7YpB$hkzd9w{Y7a0( z=DvjmoAb__Q<)aM=v=`@MclV+I^3|z9SKVu#F0x>S!RUie0S*P^Ser4^Xt3(qHT5X zsbdx?{B$N&pa(}X6?zOtrxT{)rwAZ_7mXr`wR#;CI!@Ah5ELv zRqszobVZz4GB(rmuVUr=yx;ccf8>6?JPZT*c!|x`K}$3*MILGPtS1Wql<4J6TI@j> z-LetJ@)B?4(-aB?OV`_I8?yLms@^^_raUy;A5tlmn>ml! z{b0MafVa%jcBjK}Q(`q-bcn`P^IPGNI6H7ySy0 zUH4Sd7UzRU%(}$V0{db%aBnGe==>dI8Sl9J61&Bb{7Dk2Vf9+2-4)ugk8>1mIdq6G zBmENQI1~a&s^*k8d?4-kn>te-ReZV>G@Z7WtxlZ$m9#(m-`#@UxArVoF%)5GM1 zPwNL;IsoqlY=lJrjb*4|<;z|5ft`d$cb4X?U`+FDpyjN1OB(LyHB{C>u4;p-dwY}0 z(_@deWpXxH&dY>Q0mQjCmYQ5^5zpY7Hk01A_UAgHZiYLe`0XF)NyqkeL_5dB+;A0A zwlpk`{{N3zOGDiRTJNS}Unt*j1xG9nZ)L%H@AI`Z<=V7DhO}43D;#69vd{R&mhn=*kgR`8nY)J<17_}Vl;ML** zsyH8ukhq`K--!x<#*D*$<)_kLQn|Z^6-SHfLjs)2dJtio459 zkvJbx!CHhstWeRvRl~5MK{N8Egxek|v@mLSbkR^o;0H1Zz7tEkl~(8dJ;|4b5JC1{ zq)}_RIf9LfL;D$aCNl0tG)V}FUEzS#_S#ztzqz(V|DEd8-g(unI{@_s2 z+|qG)IMIU!nrF*hZ=&Nl9z^Q5lx8B->H@Y$cL4uAPvk2gH_?ePTb7BO~yc z?nLNAGOBeqzs}73Via@a2lwa7?QjgA>%`-~x_kA!x+W#{{U_5ZBO?!<3rS1?RwA9@ za7HDUW4ze%3)X4U0=|-npY_42SOF@3LZCfd^__VXv3D(97uCt;2}y0FvbFKiayLC* zQ>Jx0ea46FVO2H7Br>g)?4(#Z+ruJ+%_~yWWm`@n2GiIOI=eGM73xyUiJ&`T%ZUWj ze2RF-r8JOq;7|&?s@Nifjer`$kDHk7980ou9>NW1kNwt5lrHZWi0?2?skf4~XE^Ke zzpV;AX8- z#RFAxRB_+Er5nd0#g2y25#$&veLQbRG}j}=a{jo(6(=m<2cEsn)~I6g#Vg#1({P zP9vPFh-_J|^W!R>7Z<}mV;%-vH%F_h)sSOkObu}^WI|vTR+pr=zBsGAVi0Cl>k@fu zbJgK%OSgOSV4-l=6pmVs)xLZ%M4xOcJV-d8yAiq6TY9(5Qd~dOoOMag zQ*k`#0d*LiX@6ozLzzIOp{Vs=j?@LEmvZSdA&uaa|Hy)bETe95?s$iJviS- z_Y>$g1$DFM?A|)O=1W`c^~4c*fx~`Mi*h$GXc1w{{JqK+gWX``O7-YQg}vgL*Y-A% zsWjrZ7|gi3@o<%x*xbea*wJAI)}JEHHwpCCvA~g^`0(;pb^%TMu~sj^vRfl*obx3-5GWy!opC%x-=JQt}A>`l7T(@-&CU~(to2hpN3P6 z#4oLcFHYiyGjVP4LK8 z-GjUTrp`Fs{~I9->+&zrr&JnQkamQ_Iv7!ckvV6%mra@wu8tyA?U(6^Re~8{(hETL z{q6}8C*{W8%wq-ITZL%}eb+0vU#Ow9{3*&k!~-aWXnxtUC?R)P9qb<7e^Nx^r6yWo zaZd<^3X_jhAs7FOp$^?j*t6KzmA!p@UmR9(Y^Wz6>um~<^GJ_Gt#z)CqIJ9E-A};c zca^XzBMWX`{+5j7syO%VA#p;1pqOxfMjoynRJ9_4b)mIiD$I~Ku5uN@%ioZFdIXbVEi%xQm z*>9!uOX9gJw1$@M5fh3HnR}2oHy=HzZb%T0OvC75ZO1h6z?gLx|M*(@E@<2pCvz91 z+XuuRT^D@Kyt7K=_=yl3N&g-VfH)v%fi*R~mF)N_eUxc+#e}N!`-|we+8CSy{C4Ul zlIXD^jr%`+EGJ4U7=}$*@V}=if$ds7vhcB%h^(|-89mcxVt3t(SNWkti{~!&?dk)+ zmxl%CUqJ^rzw#^J&8@MLJmFhpDO zeU#U2cY$u_k?ZcIWe+iYv%&3od&JWXldiu5QAnYf6CZ(ACcVmXFWdhMYNJ4`i6@%R zj_uIPyKE*;`)B%m_*xwBvJXQ~@46!mJ8|Unw_y`Gtg004GBw8g^Klayc>()eI06G_ z%te6LBq8A^hN8l=6K4QQP~O&vtP^+Va1iWtUDACX%XEPub>MR4c1y79K5ogz`y` z5|5q)G26=6d*0mx{rSc#?lk)RMhnaFMUT+G$>^^ZwYnPJCtR{@>C<-&Iw`qwB9bfF zC(~j6H-!Xuke_7hg~XBgo-AHzcGntPYlYt2a(7Qe2eLW)6tNCPz!xl{JsUuO#|@0# zZsf^D=*dx!|LsfnZ)zzFKih+&UD;aIk+%&Dma%X<3 zkSB%lVq9Bf>&GP@;k>%VZewx7t|+o@wi|Q0kjKhXGu#OM$6`TlNScN5#xj#Cz1@?s zVTPWW9Gj^wN}rBS{lmGTpe*FQsL=O8Nt#aOfmG*@abm+X#{FYEadQGag}CM?y5(At zs=KAB8j);SjN3rA%^a=7WXO=G!G12Ou>$kiqIAK}|Jk>d)BM@FvlLDI*+2g4@TqI9 zleml@#Xb4lK7o3#>`4u%y&SdJIM8{XRcW;?k3bDLp^5@TrYMVQL{w4B6v}|LTxx<} z9ZM!z&}0W0W*sBFaC&qVIZu`RjC1NJJSese)kHIH3D_f8rIRtWhRRgVBh7BqvKg*d zIJG>B>Fbu?f^s?D-sLqm%Qq}#u9;V7QY3dKBPv_si69pu-Ck)7W!qZ?fMq?GE$nR7 zPVh?TYZDaK!UQAP@8Tf$Go=k~?!F_dCMe&OEpnEm7fE8H(wLbMR|IgOOm(?tiLROo ztg_h9$PiV{hHd|{zlCmANz0WQmuF-=xq(J9+awGK*?hA$^j+_;@d6Je{J@xG(Ze;ZCDWv71%4tcf5s-%F_8ULyt|8uNyL-NoHO%UY0ojAi4c;jWit(OCqmWkT z7LllYdhC`Xp~0%F_sLfb4Dus_6T3EF3@BXezf(=pG#GZW!u%DnYj4Ps)tJ>lW|T+2 zkmV|0^FJ~21b?-Jl#f7vEyM)8{<0fK!J_xF4htZuwyP@|%YaBvY&+x?^K|N9F5o^% zb8r+{nR-2ajsR<=8C(Z&TJorQTBAmBi9`Id` zR=>2)zjSm;!0j9YZS@*~{#PwMxG0Z@EPO4x&}~cx4}MKo2a|OmWFAZIo0V>Zn8r9= zc-XFM*+5J&(S+BS!lqs3AwJ-Amn^hd&@bs!E&r-~M6#>f760X_-uxZ{sTTR^e6Ccn zN+E}%q7_mhQ_AXVmU+lrBw!Www9k?Oi!ct{6$J2JLgYGUsKzP~6&A5J8YtrjALt%{ zts}L<7zOr6(!5>cnZfq_W3+SJ0(v!`Gsi?A6o_Q?E^{`mLTataBUlPiE$1miVsoPm zj6xH6*0g!5&&Sg?Uh&R#pz3!tp1CC%iRm6Tdyo+jP@xcB(HaG@UB@gO@Z6=w4Y~9b z_^^ev?1$Yd>OJ$74K|}5{Mc}g|KwW(@I0*gR1;Z5w0iR>CRaH+FuxFH*I zz*y2Y?Fq&ohW|A1cN&Q+Z=60RWt}Gh#scmx@885P+vXw5+G%X!esRx2zqKB`8XUTi z6*W9i&3OZ^k386?J3O`~?`?D3`WuftT5^sz0oNteHHVuU3%>7T%Tz8AvMH2{cVeYC zFQm#e9T&pxw^Z;d4^-=jVdRfxO%k=>oZ4iR^&_OSR=X zhtn&rGLKSi^8mc&!&L=d#3dW6*IxaYrK9@9v!+^!j&cR7;ybkF?se1Xo3@tl3lRu4=pkZOY#+?MqVdCa7nW)9-5~!3kt&N-i{$`%S zw^z-{1Pq%q(o|j$*F?Td2Zs&G`^1UBgQpD0&cd*|X=#0vJjc1v8fl!C%OI?c5Lqlk(-gR>4_*z$P zbDpUhw&R*nMFuo)5E1}E>hARDncOTu#`K5V%)jYOXJ{!|1u_{J8<`F}FWf{dA@4m{ zY;m-d)^%5#0GLR94Z13AiUf+FdFITv+UTDb+M1dzNb}Nh8xUBiyUz0p4-KLn9btb; zz;9v-@>Qg+oAz}p{qa503?cU)JJojw;jrua7z5B&mZ*w1`@MP?%_3t+KXl?0ZW%wI z%Pv@|>^um8IN_^D-ZEHg`Pk$f1zzR~a&OPlZ#6uF&lrB85k*-ro=A0dfR>B4!*?0x z`7mASTj6>1$*J%XO5dE9O}jnpl7@0g;{cM%+V3a%VrIy0^_9LUi`USADqr6yFoq#;J3lc-incVxuI80k`)9$Y$dk z>97pak@(r%<|#U1hwEeVDO(IKqxDc5=#Nm{@3JrqBh}B&GxNPVXzp0D6tlJ=GGgNKkb%D&84>%txixg_|G(;xGqkw5D zM-3-(9YMV%#m!H}AY4#$f+_@|RA+R>NhBp%@k?7ws7@!B)(?G>=>m#(yJW-H|pr+0S_T zv&a-*AY1q)b$<^a_XEhCdPrWP@W@%X|4(;XsX+4%P<%{%jVyOsO}}ZT$y#Ov1gI#= z_&MVf`8jhFY;t4t0X&UA#b~3_Je^NIgZzH zjzC(~bOpCDdP-D+scN>@Ea_~Yt;*37eedAq3{T5<4f%C<5l;zrs~XlDGPhJRHzK;A zF*YsS6D>sa*_tzm`M{o#Rh6E$z&8N(L+96CKfGKE&!%syv(`vS4kFvR$H{0j<@z&Q z3hpz@Ea$_X>Z(IAV#TI5Hs!FvkCqEv`M+yCV~IR+Pp(*sLn(D{o5Sw;;|`ilBpxFf zYp?j!VR=RFcRR1@)5BXb%rd-?~z~~I4uvsACW93>% zIHgY?(h0B_Uu)^%TSQ34RJj0{i8%=)=yE(O$FzBJpd`y?TR(a#CJD(h=3G{xQLi1+ z#|n|O>+HI(o*zK<^oyl5MJu|=U(=TNEJ!<(aK-{O$<4#2mxAAd6VDfY0FYM@wVsBT z^N*v)Q>vm3?HSZVt>6YIwC#t=d!ix%ppE8q`}d@Y7O#11Gx#tsKIfkqYVo= z#l9!Bti2%9@?Ift-JY1VqoSJXI*}A5{DWXXynoU%jpP06}C+PNwWXOYQzP0V-QjfFjV}pBa&W08KBKSibD5=TjD``p${E@Vu>x6W5y$Zl^?J$o-3miy)E4b}}kq)!OMR zqI$XS0=m`EEEsY*(_O>(x|#;#M8tT^1)}&v;m&@bTB$y z^e5Zjv8(u(v|kZl1nFjE<|lv8Ff^C*`1UFP#%}W0y9>YkQfNUN_q`FSCf_)z_z>!l zYg0Jx)hm6Ts+xDI*-s)c@Lh0GcO=7MqMtLw5p}z-*8OMt(r_na*~1mJu6ZCb4O>~| zI?{HsU3x(Du5a(|rBC;};dxR(&c9?CzqHXN{?s_d{kuCFN$0vz=6X2<2dxC3hi{l~;pX2ERF*k|^?IR`xbeGsw8({iSqCcn=;1|34+vBrf#Y}${@gAUA zeI7XLtw1|~6_j=%H=>4*2)(6xX|wqJHX!pIR};z$GHt&oE$6m#Pf|!#O@5Yc4l0}@ z$=eog-&@RSe6miqQP&+NF?FpExmTT?>L&8ZrMPFSrFF z)Vqh{99|5Qh0I%i4PM~>X&XrC-f>hdl|gRQoFo#b&`=nWCa?;&DUFA?cbxsg(^JlN zw^P>Qw)folPV~D1dGoXgvEkC=L`*EWOJgN*H@jg5!bnHyk>1Tfuw**MW&NQ|?ad~H zEG5uP=D^&bcvDfpNj^F@o1LBQYluKc*EHA)LV8C0gPh_`m0s?=vG9U zSE!!L=So-IzF){_dZyNv<(g>^lN33P{8$=I#X`^1b)#W_vtd66!V~S~r}&OFYH%4a zAP4G-*-&F|^RTlci_ehg95s=)t7gN@YeYN~><(Y4&M00RseXM?Iu_Y@oa2B9%Qemv z_4=3fvEP=w5h*pC;-gE(!6>Z;z=UEn3himpr8Vr>uJ9XB(zsH8eOGB6tw~_{bQY>%C&VyV z*s44_5Bm0rVsd}N{0K)qXSY6t{x^MpPL{)0PtHoe@4@lzo<0ZF>jJnx57QbRBiYs- zn60x#Z8`S2tW1;a6E9QwB-(95@6~~xHHqLS zs;gNP0!{qh+V3Z!U-v@0J8fzTk<#DiD}>)L$W1(Om_aLgwxjr9qfo4$E$!`;Hbe|> z`R8!_CCzW=QS)v20_QyWF!Ddcs`U3e{sy`;APIRUtt1~MSb?1IqeL_GJt&G1Ub?Yx z$zHv#&*(QI=@cB1_=L5zPyWme^V(4@tvIB8nHLVSQn+k!@ZoG#`r@~ zje&PIAaS$$u7WDo>~jHB(#7d)=X4p^|A#g|L(>JVldWN$b`@H6Tlo&`#hRL6L4H zFh#0N8RQciDKK8BNEa6Pgct-JviD#ul!)6kNq>0pwb}Zmo416pK5^%-NS&UN<|ZR# zm|@#T91Kku9B($8Fh(U+$69RBD6!M5VQLt+9?Nu7Az%sYz_CP zPj{b$!G(Jv;b#Zp!?PXicpRIyX++Jo)0=r02Ps+iN&TiJ_n*|C-Foqx3@>lo+tK#1 z7<6jGFe49b6+SY}ganaf(YOSndW)rvQK+K@_)SF^^SMvImNz4j8LZcY@NX}E&c^;9 zM5ScqK+(a<+p(;XN3>0OL%?LT1dakML9_B@;f$aGQlFh-Hc@h~FR9^wVHNAxovnje zyy%xIFl;}4tzw%USpD!UWCCLlI5zY^7ZJH6 zhvAISEzH;I0o*96%e!1uQ804muyIIHDrD8yG@Cpf387*2>v6hV$-66?5OG;(pB|Me zGU#0_;QN(hw0ZSSem-9hUQ>PCU3?x4cK2R`hS9WZPQXPTv>*>?Y1{)ZX@p5rOh{@b7Eq8SOyN&yWxJ_IwC`IL&LkwXwtigf zN7{WiSn(WV)!H8iIA;*m37tFbcp`_nzCpNH(X_w z1S0KUt#?T$j1MBE{Aqa?N;;aA`@i?HojG{O!zM3I^>0cna*3c8O96nFCPHcKo~L*uP^P(kcJw5qD_M)1Y~=RU)mcIFM-?(W zv$0sni?^c9MYfrz)JpxI=M!0{XigXLs&}?Vl9Ek&j1on@msU4LR_hDyvV{J}PdbP- zXr?Y3j)4dZDs|!c09l-t!;G90KMLVjm7LH9ng|Vb-#LP=MukonjQJzY=Db+z)#eDO zKLjDco|1H}!qK&x!^wEAod*|0AC0p?`#csNjh8~sY-^6|^NK2|K6EO=*EVnZSHj9B z0yes8;Z{8VB|AozY!)e*QkNMNP|C82(hs3jxtT6O;_ z<6t(!^l6@p_xbi`l((qgki*rjCLd}cO2rLbV?XyMlNjH9rJXiA6}qq)?p*M=s|=BZ zM=LP&c^wh?`~>!VonHIBz32W21qO~RhGvoq>wW4TRk+K1%5{S|=U3Z<08 z#%@Bg#IVMg)+lMPFd9~b=UB2DI!erzO1w3ciGONYZ=|7cbv`>9`EWW#H_~s3Tq~H> zrIjd{;((B7Ez`&4YytWu*wOd;d-I=w>xS#eC#k#kNcl<9Z{2c_*em3_MQEqtdo{w#Q7(}w%8g{-$X)Vws96q~|DdtA&IuXpvBoa7oo(VNh~fV7n$zmx98HRa$-PR+=bbz$<@ zX1q^AF4^ac+~ehw&l=Az@CnvvNHroai!Bn;KWh9ut>Bv8vr9&?rwvg#uTlK!uH}VD zW31g8i9H7#DBW0cUcqQaxAejWpuT#0DhGw|ngQSH4wkG= z3HS=NKjtq(xq+i4L7X^~t2;Y3=4l}}ri8igqD3rnKg>2^5{{iatX>9lN`xLr9LgJU z{)saY{s5{(%y7@MlnTqv=u#+Qe8$VkCHBX&;XS5KIcLP?Ou=bzq ze`~TR-9H(!utT%dQh&aN1kN>lsoam#%iNKP7a&7An`D{nd7%YY{5Y|LoOGNX;u}Pu zXP3bHlvUf3fi&@KE?0l*AoI({sJmvbXzNDH)M@$f%DL7RAqGdeO+BBwzKCA3W6oEd zkx$08C?$t$8?T?x`8ta<+a9RUK^uC__5C=Y6n54llnEHDmkx$?R6HO;GZ-Y z&xuXeBI;eKhh=4-X`aR}$UH3hPRBP9vk|0_7D@-~DHNAZD77f8Vkd7pg&=o>t}x{( z3yp~#tAi&cQYm{+zb28)HfEV#ZGje(#X0PySyGb!+V*48G5_K_WFV9}fEnF?CFk3} z2>IZj_ed#4k|APxrf6R^Q)C~tEX_P6VcV=@R;oY>nd4@vI!jc9QpIiRNQnMRJlKi= ziWt(~@DgztOSMq7k|Hy^lc7Z1cRGeZVBVO+LK{vebu<0U^1&8G(hR{DQHn2RQELDy z@DXcXaz-gTZDE-hlqTKr^PVBXz@SKC?Atb&MJn29-I%mF6`o?&R;bnEG~{S+8`FeR zLcu=yYbdt*dBM;*VpQfpF}I2HSI|1+GtXSA`Y9?bu~23%f?-#tL2l1ZXd@v&F=m@8 zF+Vi(Quw$aQ0U85JyStN0pE$*Z&VZMyNG5qvSLa}-KC_U+zCTDyD;s{J6PF_wt&kB zbP+cKIHkhjl2TBnge}ebYHB;m78L^44x6qU88eI_vH84%^jFqowrEErA+(Q(+wSC! z*Ye9nyfD8mp?HNEw>J1dKj=2_p$$1o)J_LK)_mLu>4G0OY2~kxZ5}tsg}D843>b%O z7oyCdDG|%G{SmTF)kLHJqgS8FSHKLt&Qgctbp^F&U%|8&k4wSlQ8~U2TyQuPHTTVf zX!@9zn^eI>9FvL$I0WLyo$Qyck2HICY26IV6`RrdT)AES@*>5*xv>M*kU2-Yf4(+Y zkaFXHEl^b>`p|H{wsOaHACjZ6a> zfG}Sprr1d`3;(ark1Qq)!iLWT8V1WR4Ppp}g6^N@f0eqUDEd8(^1svmjrd`(r9TEB zv@NCin{=)4=9L%ZEt;!0fNx{(T+%z!N~(yAx@N&2bJuE!%Sa|fpN>-|MEan?kQkYl zmY~7;s3D>_cnmW$BMTtyvcZhD9;79;?A;cgMBWujHL-2hF%^u7g!8^M@@;jKN|kze zwrmPA{e}y*8kA&4A@UG9+gGPH915Jm8Z~8m2M?)AaJ3<4h>IP+iA%TSyWRtXzX$KQ z%GX!9d7k5TLXSE8kKGWH>ve${lpV#X!EXsl|IgawG<|hniU%j{4&^m7%2Yv4YSG5*ve&RmFhFh z+|fno>3^!r26S2InOCJBhMBsgxshOp$x-6`>=KQ5i8*W%2N90$$oP23KTrH-OWJk06Jt=9)9-kl8^P?TqmZ7U`07_4GuEQXG@0RSDv-aN8r zUA?QF4Xd-30L6J39OOym4DgsF4U;Yb@CttwbfZP?ieBV$1m$$p3VK#;5Iqp0yfHxF zFL0HiC3{e5xXL>f69P2bjh~lPOO-!YZQIO@p}bTVoBXPNPai7EMJePS7sZ4cq06KX zJugu^B~}UCoirD@)tHz8_tT&#^4Z4PEAlszs?D=P$?9d-#^h_BeA0DZ|KU3SwmAmJ z1EdE*BTGvo?VqB?lGR|JKWC8YUkr)?F__p&$tVe=?HPN2fmcM;?Sd(LXF2O9dr(|L zRx;O)QQ^`1O9ZIcGp6GUB!dF}TQq-iz^_8PRuBqE)!yDBOGIdN0_l+KkU&@@3XU|g zBi}p)XKYGjhZc?>8dXU-z*v_Y;5(Ui_VQXIRaalk6$$L2K%IE5_5lfO?zcSY6Ae(k zL{7|$iCi>yDv{bn%kwv3yR`{Sy{_I7=QQM{{#be+jNh4Ienj-=MG%`OoDRQF_LN@D z%89D@o-X5?gu-Szja z6+k~;o~l7aD2cKRO@=&6tsC7Xn=%F=<33S%{`ez60`LSJ`XDiMb8eImaRP|~;SE0C5C9N2y+U7F!?j)hfxCFuv zzdS?9xx{%LLKFjMjQP!GA}QWQb)p@YVmTrNdd=qF6E6Tc!k?BEn?S#1~!qk2( zWPj-5pmzI+PmPxlpO>`cc~c+d)iRTahw6IgNtcu;Fs@p1mYa#cDq&ctVB*;7oCOS+ zz7BVATdZ}dir~GmYtGV&_v^IXMKW$n^sk3#y%}+DT(ug_4^6T+BerjfknxG^z1HDz zg`L0%J5%RiuU5_lk+q1()oGy?pZ_!J@-PVy^r6>z-+2`xs6xd;nNx5 z+AB+M7veDD5pTDx_IPd`7}0Jm3thk>bPRS<@$t(llf6w;U`V$|GxQbH>)o=U+tD0@ zdGU*89}LstfD!KdYth1D;#qFTJp>_fHghs+V5nj*ZknT=lF0Joa}cPvm5nTYBSyo_ zI~z!j0tN#bG($YlchmbZMLrd;gntpiQd~E;{~W$`b9#O2)K=ht%rQ38;*zxSq9XzX zWh6YW?n`17pA#Dw*V{6dP(FC@BJpyVdel~`krG!SHpxA?Y-bB@YTAj(EZ2HN0u&+K zEyrR7anOk57o4t>Z8qosrW;-oQOb_}nuqoNH>XO_cXw(KwB}}yKUb>abJs!k8KOtU z!6l>d7FL25{!;=dN#@NyICfcbl7$5TQM>XE!7w@s;@*;Pnn2UXlkhIn< zpLyp|TTCFmh1`Mq;I+gNaFmEoTNjM)Uq6_#HfK#rNTU$ws9 zwMYx#cnZPh=|fMHJ$NqaCrt2{(_{kihoi1LEG1p5t?bPLO#^p~u_=2qoS@|voGpGd z?r~4d5(dJ9bxbeyD}7p?kD6KaR+HLkyH7o;<92TcmF7P26&@#=(Z#2Wr$GZ#FuTn- zaG!^cX=TNpW;2`yWkUGC+bJ#<80r6M6N$Q$Lh#b@wxHp#|EGC4R(P0)dnh4|OBlxV z`{LjPeDeFL^eYX~Wex>w!oH&q9B)K*I-ChRRsx{T3pJ!&-hI;lM!-H$=l%`z3 z69UDk?SMA}yAl5RMs&zq zWI3nVL>5ac)!}opO;7DYjOr(Wci!OWXT8~p0p`dkQw^%*a^~5@kd&+pYYW+;9Tz#= zJ8&2I)3Gzjyc6~NZk->rvGk&^mC;8 z(f^c*SMTzLH)gzrGS>{%E&EVJ3+Xm&)K0nF+NhfpzR314qq>{@gs%vNFS&`*=U!6* z4WkTq^{?doX@&Beec@u**(7KRMAyaoXb8eZ9<>!j+rCX(9vaRc{x9UbeXlT=&+d&w1GTdJF zYVL`!Vd((?nCEx@Wb znr|_gSzi#DRVwdx+*R_Cc<`%WM*{V&P5yo&X6MdY?t=OPakrj-h1(S}-hAu<6auWu z=|7)g1;J8FjQZN!83ka^%bQUvD{89+Tl9#Gb@FszmsIQp%=ocKAZx4tz;l8Bad9z# z<7`FJ2Cj>!t4&Y0N5t{(AV;Y(YM$ctnHT#yw3$PXXy%PMM-02*RQ>(F1D&7Pf6XE3 zPY_^4D}t#HcDU@bZLtj+x%dh(1;LQjZl~*5kxMf*D2}$||u9qUF&`y8I*b z2!s}gs;V3(GTCW@!`!8s*W)&nX1aK`lm`}MKkNk?7)XH zoo{l%JX1)zSm?}*7O&xNTtu|fat12%0SxkZiZ4S#E!M>A@#hIAb+J*zRs6e)Lc7OP zc1tnHd>{diRwG^C@gVo1rgfrMGSm6*k3npBfRjup$yz2q?>zA1Z9Sa=jpa9d8j!w9 zxsZYTH^FG@2T}Y$pW4#6ODcBWGWjxOL;=AVWJPu z8JmaLMkFVmnpFqwUw7?{2It2!=LA;WZ-#kOyTDlFnSP%KyHSW5MI&WDHy*ao<4&8wF1lpj+w$m4(P|NoHN1- z$|8l?R8x@zEEiD`O?K@(ee{42Z?>t743xgZ< zy3P)~i?sVjZgy_fZbR`L+eM@F2qo7U$QeQN&mFP}E-d|uNgtom^I6@w`Ol2o80HtXR&&<(v(FB<%dEpOh=q7_FWI-Wu zyU7r;I{Y%9@<)-XiK4HiB0$OgzgyR)J>QV&pp0yNM}V^Wp2Y^u8t4hIM=AXhImZmg==|+nSAieoC~WWfrv=q&)F=fjw~8JWG?N`B+LZWyW-ES1 ze&2!cvXN9SbjA>?MFy$ClP>+Sh-1lNC|TFID_Nf1=TW})M^F2=d^4^yO)5@yn-%qg zr8aHWmGEq%G73~F@&Y^RBd&6F$X2Cm8#~>avT#0fo;`)E5uX2wz(vl0EvK+Fx;~8y zw?SOhLJjqds^=`v%977AkK0!5`^V!QQg?DDGTh(f_M2&6LXr1Q1VKlowf!Al7*4(R%*9S}e_33n!ZGM(3AV){Hf@{`Ss zL*|s;yC1JKnFAi0yoa!0^XTEIVXGR$jMy;ft4`kI?m6GX^@0*GIKSfYeO7-|4Mso5 zKkEVmp{Jp6^-y{!Ox6WaTHW{}Ow*me8XNIt< z9Q|eSbi>zX?JnsVivy zMG6#`LUDJui(cHJMT&ciyX(a%6nA$ma)FDxTXC1-?(S}neqZwPUh@BEliia!n?0M| z?3|h3%+8D;Sp~m6ODu#TeM9a0U^Ld3;&@w3!(<5|@teW?+Os;+&Yqj38lqF= zJo{{-iS`86i;o4cxX_KZi&we&w#=}=9sK0ixL07YICZeNp0|!&EokBwz zP=%c1i$CFu-ixp~ZZ1=Dn(MkJX+AuWl#G%}zvF@rl?F9BDEes~;arFNJ{B+^sD$tD z<5UtM8fO8+Kh&vfz#e_Db)#q}|M4CuAMfpzyMobrHgOh_{SH8nc?lL2SgexYg4U?$ z9nZLrgPB6%G@k&-A3Z@LUwPiTgma$AQFXYE*iU`4HYVP09j~%w-ps#Z; zS%M^UFG*B)BvCj!?XVi=tyjt7rg*c#OAQ$pU+OtpJ=1A)&>z9@_AfSCnSFBNP~Fr} z8_ZiC>tyyAv!E!oOz9LgMYJ7GQXG4Ku3vV(MU>8m?}PfZm5`nEA}q-poO zVh3&*ww)$QUwlEgXDl=vL$g2b0ked*#$C5H&2&`bRCL9o>Y+6da%ON(@M?q5#e1h4}*-vhhMc1w`5OikRXfpKr!=RAbG!Hj&fN7~IR+wRG+ zbp7>ZPs$16%N*{cTmp(7Sa9hXZJuqr09QBETLRB6T;D7v5zp6YHyQ?U3au9aweFV% zTV-J5WBZmzzZH#u{w^OPcZM?afms`UG6k7aO6g2lz^1KsZdE_~{U|9|;&!>mQNj&O z`#mt3cBhJ`!1{J|>NECPB=aqipoxa=X2ePU&lU2U$@`7a7;K`pB1Xd;L*JS* z(i^or-=FB=9h46*-%&^oq&+@Fwe9=;K0SvL{Ooa@_f0oO&>-wsP*wBCL#qiL4nwtrld!EUR( z{VW{?LtxZ=4St6OWf@Vs1|0|l+`0EL!`cv8OLpcA=O~yv@Lb^McA=bKf2ufxwz^FJ zabFXfkbk__$LLVjm_g_OvNy2PD-?>U+KSg!!IUQTOEHi@X5A>iwt=O z#sv~876BDN#yn<%ELW68HsE;KVIMyIMGDPY`?nS&MlxnqqDm%9S!yw1_Fw!B6kyM?ob?*qv^fBbAjpl zFa6)e5UMdjnHr*}aT6f24bmnXQyE>t&X|)Gm;ZoXHL3}LQig_x(*37^oy)MGMZ@Z0 z?$GhYHb~lfU-LAE0^ds4GVDRE^#L~3bMq7ZDcCOc<0jh@n?i_rt0`5l`nlXTNmzEZ zz(OdgqbnVVB!+INw?Y(a&pI}1EeLZ<_M*r#yBuY0C>}^)mK8&DW_avf2T&NsF@r4o8Zqt9NdrWs`GDMI5|E!m_4&kNN=zA z!@&OguOZjr7zezzVmYjuIi=OlZ67ux=--C1Fm!J=j87|`HPIku^goRWFfHd|(l#bq zupxz0Ejc0?KVkfTVos2mMnQc;{Dw`tdt61<~<1@tCjc5xgop-y z?jb^bKE>tzFg21cM3}k0oedsZEfJm^&oP*j%^M1v#C>Azt0wZ1Y9ICKjbQ-w)6ifV zW7rx$4qp<(&tq2lU1x+Fqw04zUWP#fT zBq>`qf0SRrRPd-tNK&Y9WIwQvu1on#>I>QnN{uIp%S5lR@wUCCyx~wBLa`1HnIoPa z1798L5J6Z^%`k1jTQzI;!MNiB4o;=99tAwo;c%b@IpPXE@D(l{XpzTE`BKQ; z)!Q3&NBeEpYpBl3Tgx0YarxVzq*h>t=T}_t@-AnkUc5ljk{%`AXwzu#jbvh+w?y0G z{#!5?3AtH#QX~E7biWlEb}5W9uCki~nDJ*HndKfv9R>gcF#H+t?Bj-I&u%ZTzQiV- zL=`w{cybRe{?=8Tj18hVja!auB#S*@kd;z`JO!e%svA!1MN$9q>a_8t4Rn`5Mp$QN zl*B~Zn&<8M>`D2CHzLUDG=~(*R9~$w;R7!5s+?0@T`2w2WGD$)fYPzYJ8^ZqEWyXJ z@n1m}3eED6Scr)Spbg*#cZWPcp^TS{afXcoAvXI>nUq3-!kobJ`x?5vL^7=K^@Z5l z6A}Ua!%{#=S@XKjdL!Uk*@Z!USin3P?b)<9U#`Y~I(E7{^6Sh~VLZW9ZivAAJr8B6 z+5qk3S7rQfGgb|Gn8Z_2$q#fKR|j5yqVcV_dU=LO8wMF#Q5VKbYWRJ#+G{7^UAH=0 zHe$HYdp@F2(w4?0%VFJVKystRiL>>I5kA1cD8j%5gv4RveMHHaHpwRY{JE+UgFTtS zFpe3~4aGlpSYJy<0?A zDB#Ednr!opGt5d5K${Ex&sp4k`U#6Kksg`aV@UI(M3W&6YcznJW9Cb)q4Vt>8x~4d zP`CQfeA(+AXzbx6^N)aFqXw;S5a7uM?=7J3$cxh+I)(d37P7BFXXU^XHJVkwN{vd} zqO+z(x2ycJRPz;VXmp-6DB9l z4u-SopAiBw)f(i!q*Bqn^GLLezBv;J=as-=@^indH0euQDJZ4k`f1Bwr)4bXFzX81|lOgbno+A8Kj4cKmXX)^o*{Thbw|!#@Wp<9u#D{R% ztSH9*`P$X2&KV@LTYRFCKD@e^Y|dpnqR`G|U^*pB_aW`~r|F5Rpssvn_WUyt3T0?U zZ8e39MiA8SYmevSWu}5~yjW*x`7FLxMZELTH-57pyECthX{nl~lZvdI|HXS}x*o+% z2GEahY8!?;?=cKs{sZ>5Fa_54#06`FYF3#L6ecLB*P@2lo{ zNDj=;OEg(fSrMI9z@-QXp}+N>`@@F+IZbm~=a$jhv@>C|?w5&_!Wr#~{=?7$$7y3? zvkli8m{5`dqg%-%@w~h5tN4q@w?|v{nB zzm&HdB%F4xaCiA?AQ=&cZQmz>urBQ#ll2jCg}h5#79xlG9Y9g z@>E1Vx2(G__pq`zjrFAeEDN7pkk0Q>%vD%h!X!xEqKEVoy;=HD`ETFVRK-7+m!L~i z^nUw+*AQFw#3+P$DFJWAlX|K8NAs_Ksr|AU zc2mZ<@K?^`2#VdHM}68s{xl1%p?2Rw3x&1u>tFwcnWo-hrs>uH2WFb4^d-h;6&GU_k;Wuo5~ogX$7a@$T@0qL)5C5_dq^ZK-h_CaN52UL#~cM|mhv~*k< zLJ&iR%DwTjj^v@TgyixU*#!PC+vbfEU5b$PCRdZqUbEU>P==v4sp?{0J{^e8X7VCm z#%AEr&4Fha2x-}QR_{l5SAL!PxUkXK{E{FPiKO4LmqVwB!L;({-I#tT5x`9nUlrpZ?D|RSHJ#2hjLEQ%9|DCGcF)>qE9iDiv{w0VA-e+`~iH%pNQ z6_B{!hb$6xfn;$PH|fORxTpXXZM&n>nAR9k+#ZH6Vw4qykBm2&n>XeS29CBmFxI8H ztW3oiG5fC%`M)s+u%!OtSC7TG1eMi%8RQpFVmp7t|IfzQ3J(9V`u8Qxs{Qv~No>`D zX5We52@mLlzC-k64|W&9&QNfEoA)YEN<nCGGln7FskzW?9G4Pd#hzfzha&XH55HT;|L`|^{r zeYr16xtZkWXx`TyePFeKHj5q~gI$;L_TyZ+L0v*3-RZg|pr z7+;*b%m^ek%^M8H*%9pU?Yce4rV!h!yB~fPyF^*%^%pUq5t@)#B+ z4)bX|cb%ZHjZ1XkjanIq-ToLQ*u9QHVRk1ir zbE@(()AKoXy~lrm8df*3vq-#0h@8tmII$1#0re=wl_%BiAf$LLWF3^0I!-46UM`)E z3)E$yDFaD=wCPi`10S~EjA^fy=s=pQjwUTsWBYUYWfsBO{k*=!Qpf0DwEw`4yzJcB zB>{|H6Vz)oy3;lx*E5PKFP)7y-yD^XqYwX`;2gx-Vm9@fnjOe96p?EWqtn?t#>kXwD@Pvufa&g;U1pDsM1k67F~3 z??xb3EYh?`ia3e-n4IXab9`gunC)ooeTJ1$egPraK75dCzS>XDL`}eJNmha4#J#i@ zHJD2R8r^9ajO0H5(cbs(>?!>?r23j;XnQS;C8Woiv-(rY3hT)6s{DxPamfxRLYN*T zYPoR(zN$1tPtf-GFacl4Df|y@&O_X!K*7>Bi8&eoA&nGy6eP3-Nc~7{=cdQLoNN{? zWWf4>Z(iNcyEc(QXw{Do>~_gW87qxik-6aPF?y+KZ6mgw1VlK)|CyXG1WY|qgcU~! z>H~KY6>=m}oa7uTv#v=xz$(4b#yS^KlPa*r>dV`$R3QO8HZ4~Yvh6Dtjq#QVBPJZl zHMl|IWYzf20n*$7m&gOAmGNlR`26D=)%eF@>M_<0y*a(|+a-Q1aC(O$F5tHBu0fyn zprgcL6!W@yOhgc8^q~c8T1iOSImtTpjcVPOSE(i=2zwjd1p!~> z^i7NEm7nM$!0EgvT&Ubs)c(AdP4#mp>J?|)h#8l^zrGcX9oZMAwN%zg*@j`n{2;>m zU5kyOpot+t9(i>Je_|6G)CoGkk8gvK)O!&5)r`f>NDZWg z+#gT!yA(;x`Si6oHe^(4qk$U;c`_235&9*9p*LlD_G&}qKj${>38>w)Ka415Zak*s zG!w2{2BoR8PTD-T4-Hh+x5w*kme=yGZPIfceH)Ot_WbxWX7QT_wZ5gAo*id#F^X?S z6gAbjY~=z6_1_qA!91#6HlvW4Uo|G+Ime7&;JM7w0`QzYO}NS#um@M?uy43?k~HuR zv0(1ko9w?T^t~D%Eby_eM<)eM5Kbr!t5xJX5DIk0%^n%$NS4k;cVHIJ{ed!4dYN}^ zxr2y)uFz*4&aNE>_AnW>{XP9$t*=6sc=1Ln&44?sT~P}Cxn7@CD5?HcSA@FoZBBdF zV=cQ+OhdRv=kQj=S?!XhetCA>>vQWPUkLTM8{b}|Yq)T5R6Mi`3judP zlL(70|MR@>P!KstR96irp0d{xQ#$TTou++V=PXBZ>3e#c)22@7(w+i0Ow#V$E|>CQ zxbhrq-pC-!+QBR5qMjt2`; z5?AJVK{4WfV0_m9EzMXKBT#fwEudXE-2$?oj@;~$=Lv2;=rgrUe=wApcP9(S9){DZ zJ^9TMLx!6OvVj2)eB$Y~E}NQF^VB|ffcVRH?`!1Ny)GIq+M3Z;M*C5TDEl^z!N;kJ zvRi^G9G7!~n%|FmdF~i8@0K)8_DEnVO=yCEZ;Wmdk=cgKkR)NEFAcER5+x^N*=9B=fS+Hay;H+0&- zzvtgq*|fwo`d1$V6yM(Q%~eO(=r*Vwvw37lF&%6FWLfDALnz4cN|@2=f#b7P*)nj9 zq3_{{&Cr+kmO$IALS=juAy>|%jb&Qa0j3+#oF!d%P9%rIb#BWOuSQ%98?pJPD1NW! zVeeDMay;86g1QoWvAa}*jECZ=3t`a!|-;}6xc9?c7oM%-#5am*~zZxm%Ayf}5^;49&e zcD5)h1%n;)JbEmTyJx9(5Bk!Bk!#ewPeM4{>;5r%F52<70}2j5sMw~Chh)6c8~J1e zu9Ke~vi#mkjOxULgjE93(%KEeuiQF(@;5feP*)WqWtvGep2^+MpYxiJE&eutG4Rzv z%ZGXt+U_)A&TX=qj1zxkJW@jvKkNgzt|U7~P@*ea=HVT=y_@*!zq3hkUKSnQIlbh>IiSkgn;Pi|!r7f)rG_cW`gc^AOmDMsy4a+;LlxS2F|L zyi*(`ho88+BG>!A@x6QWJZ1kh&0s0H;j;e0=(|v5XT);ezP46^$tLLa#xraHQFU5E zZS5&5c%^U@UtwLczV$a1B?zl({PpFmYCYL4Nkz1c?nYZje@!WiZ1>(#$rw~^!ep^D^^DJS`W?>uJ76INZluU}2jB4MvTjUy&py!@@ue z`5?i9K@n^~5N!@!3_TNNU)b+Yg7By!JW;wc-fa~))Z*lB1^X2hgKr{VQ6@U8<@0YX zO3f5{C|9U;+;il6F6<6?C^WPKk&J*s#KP;OLI4_FH?62=jB9_QYbM)yw&C9|jPT2h z(IVc^-x|X_V#uWGj$3ZMETOrx5viV1zw@>@;QSu1B;}Ci%$zANZ#i0<5=1vVr;tNM zAYjjDYbR`f+=Mb4qAgynD#SkDJNRPteg4F0(WdC`)+qYG6PZ9hFq%iQFtGnsPoex# zM=!4yn^5^0HIFYm&v-qEYt8&CCR zYw-)1qsSBE^MTbyk2{mSzv;P_bp7F$xD^MU1Z=yohEVK%l4&Q=mCftl;G(> z+^_UBlI^ln=XZyFVt=pCK<%D5zasGdd@l!fTv;u1crdfrn+P|&O1nC%-Zu=BE)6Hy zm}9?RKqoj0%Yf|jTv#So2AubkzC2D6zVaB*zDOM(xjc-2&zy}Q+3+E!6KYbckfwQj zsr1oe+O^s2_HI*Btm7%XXk`~`Xs#j9PIbV+l0kGpcvk?NU8!j4ryh;Ca4>B~)EBDm|Rbmh-I#ODm{m%|)<3%1auD1CY7W>%b1Q zwn3Vmj<>d9%>oU-sC*R$`jJ9m|5KbUvG$(fq82f4HoLvHW4L3u+ZxDi09*dGO_~;4 z^QwMRz3SUY?g>+e)!j5?5Hg5|ow&9|AFKtOg^Rnz9}%-6Om#CHWR>x0e711Fmoy5{ zZevJ)Mu$&Xl~!d7`CHjqM$H}CqXSfvpDantMC0NPJLd|N`c{0j)Xhf`9lCeL^G6o%(Ad?5L}3yJ8T%p=JMDsHC>t9b&f4?3exd zX85r|H@`LCf@K&8@_Xu}I&VW?>Vz-rPqceP_oo^tG7NyXY@`PuU+}W46gZOh;@DwjZa_b8G`jeniSM~se=ec~Di*1J&^BR4fIc+5`?H*|CRjNvRF?D# z>inB!#?1|6Zy-EVXZVRn()_=#N)PjgDRG3FSX-$Saww__g z3yfZe{g-v5t9<5K0NY<3BluJEr--T*R9?u``>YcMHReYQMQ>VET zzx;GBQH^g*W1skysjh-ogZ;6xU!9eUo-GXJ=YsF3b>!IcTIy{h2dgRYM`hvq5cn>OV`-| diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index 158cb21e0a..1fba117053 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -39,7 +39,7 @@ }, "dependencies": { "@kobalte/core": "catalog:", - "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13.tgz", + "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", From 7534d23551f665e65080809975b4ca5c7d63807b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 25 Jul 2026 08:21:12 +0000 Subject: [PATCH 011/129] 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 407d7812fb..db0259a67e 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-0kcwV34P2C3yKg2eG9W2nW+OedrSBb+1TdpuUeYtauY=", - "aarch64-linux": "sha256-yHVygApQchAB34wrtFR4GU0CkmZOlLsl3wsp15u0xzs=", - "aarch64-darwin": "sha256-DyalcwyK2Wn5R6249keFcNVECbgtjYNjscOFqTi88FI=", - "x86_64-darwin": "sha256-BkGw0GWN9W9q+/g4FYR0MqxUuFP80BPoERO+ypz/arQ=" + "x86_64-linux": "sha256-2suqkFAUeT2YgppKqP14s0C+GE3TR3uNKOOD8vWEiAg=", + "aarch64-linux": "sha256-cJ/XYTodYmgs9ZFQhHQbfZPqxjIMmEvz062OsX6/6p4=", + "aarch64-darwin": "sha256-tKyqHeI6r1aHEk4vBS39+eA6wh9KcIwsa2ctcktKkEc=", + "x86_64-darwin": "sha256-1u7+dNjRcqKSvUTCEarVJ9fMrRUyg9y+4seZAm6Fmus=" } } From 7d8195e8fe32f750f4437b314c8f9abb34918f45 Mon Sep 17 00:00:00 2001 From: Rahul A Mistry <149420892+ProdigyRahul@users.noreply.github.com> Date: Mon, 27 Jul 2026 08:16:59 +0530 Subject: [PATCH 012/129] fix(ui): keep mutable selects open (#39027) Co-authored-by: Brendan Allan --- packages/ui/src/v2/components/select-v2.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/v2/components/select-v2.tsx b/packages/ui/src/v2/components/select-v2.tsx index 7f9a2b5d37..57348ed392 100644 --- a/packages/ui/src/v2/components/select-v2.tsx +++ b/packages/ui/src/v2/components/select-v2.tsx @@ -121,6 +121,7 @@ export function SelectV2(props: SelectV2Props) { {...others} multiple={false} + allowDuplicateSelectionEvents={false} disabled={local.disabled} data-component="select-v2-root" placement={local.placement ?? (inline() ? "bottom-end" : "bottom-start")} From 1ad63cfc6cd1bf19a47b155cd2d9b173014174c5 Mon Sep 17 00:00:00 2001 From: opencode Date: Mon, 27 Jul 2026 02:47:42 +0000 Subject: [PATCH 013/129] sync release versions for v1.18.6 --- bun.lock | 56 ++++++++++----------- packages/app/package.json | 2 +- packages/cli/package.json | 2 +- packages/codemode/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/console/support/package.json | 2 +- packages/core/package.json | 2 +- packages/desktop/package.json | 2 +- packages/effect-drizzle-sqlite/package.json | 2 +- packages/effect-sqlite-node/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/function/package.json | 2 +- packages/http-recorder/package.json | 2 +- packages/llm/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/server/package.json | 2 +- packages/session-ui/package.json | 2 +- packages/slack/package.json | 2 +- packages/stats/app/package.json | 2 +- packages/stats/core/package.json | 2 +- packages/stats/server/package.json | 2 +- packages/tui/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 30 files changed, 57 insertions(+), 57 deletions(-) diff --git a/bun.lock b/bun.lock index 839f288394..4f969c2f1e 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", @@ -96,7 +96,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.18.5", + "version": "1.18.6", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -144,7 +144,7 @@ }, "packages/codemode": { "name": "@opencode-ai/codemode", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "acorn": "8.15.0", "effect": "catalog:", @@ -158,7 +158,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -194,7 +194,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -221,7 +221,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -243,7 +243,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -267,7 +267,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -287,7 +287,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.18.5", + "version": "1.18.6", "bin": { "opencode": "./bin/opencode", }, @@ -381,7 +381,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@zip.js/zip.js": "2.7.62", "effect": "catalog:", @@ -435,7 +435,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -449,7 +449,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "effect": "catalog:", }, @@ -461,7 +461,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -493,7 +493,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -509,7 +509,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83", @@ -540,7 +540,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", @@ -559,7 +559,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.18.5", + "version": "1.18.6", "bin": { "opencode": "./bin/opencode", }, @@ -690,7 +690,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "workspace:*", @@ -766,7 +766,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "cross-spawn": "catalog:", }, @@ -781,7 +781,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", @@ -796,7 +796,7 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", @@ -841,7 +841,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -854,7 +854,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@ibm/plex": "6.4.1", "@kobalte/core": "catalog:", @@ -888,7 +888,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -907,7 +907,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -949,7 +949,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -976,7 +976,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1027,7 +1027,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.18.5", + "version": "1.18.6", "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 75b2b8c488..d2219740b1 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.18.5", + "version": "1.18.6", "description": "", "type": "module", "exports": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 26a6210281..d8886470a0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/cli", - "version": "1.18.5", + "version": "1.18.6", "type": "module", "license": "MIT", "bin": { diff --git a/packages/codemode/package.json b/packages/codemode/package.json index 15f3910e27..a57dad6130 100644 --- a/packages/codemode/package.json +++ b/packages/codemode/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/codemode", - "version": "1.18.5", + "version": "1.18.6", "description": "Effect-native confined code execution over schema-described tools", "private": true, "type": "module", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 22e440d0d5..0ccae42dbf 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.18.5", + "version": "1.18.6", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index be70f690c4..4e7f765851 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.18.5", + "version": "1.18.6", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 5909638d05..edb98fa95e 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.18.5", + "version": "1.18.6", "$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 ca8f1dada9..26b7745a9a 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.18.5", + "version": "1.18.6", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/console/support/package.json b/packages/console/support/package.json index 3fb6d2fdf2..ee4bd72911 100644 --- a/packages/console/support/package.json +++ b/packages/console/support/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-support", - "version": "1.18.5", + "version": "1.18.6", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index 7ecdd92632..b297ac569a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.5", + "version": "1.18.6", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 9820c107cd..fc7d278bed 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.18.5", + "version": "1.18.6", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index d29305faae..84c6cda131 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.5", + "version": "1.18.6", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/effect-sqlite-node/package.json b/packages/effect-sqlite-node/package.json index e56d3ba4f2..fd915ebd3b 100644 --- a/packages/effect-sqlite-node/package.json +++ b/packages/effect-sqlite-node/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.5", + "version": "1.18.6", "name": "@opencode-ai/effect-sqlite-node", "type": "module", "license": "MIT", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 761f1c7589..f338cde8fc 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.18.5", + "version": "1.18.6", "private": true, "type": "module", "license": "MIT", diff --git a/packages/function/package.json b/packages/function/package.json index 535ee6faa6..117632f768 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.18.5", + "version": "1.18.6", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index a39ac13642..40e0dd80c7 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.5", + "version": "1.18.6", "name": "@opencode-ai/http-recorder", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "type": "module", diff --git a/packages/llm/package.json b/packages/llm/package.json index 6afa905bb4..100aa85526 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.5", + "version": "1.18.6", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 7960a72dfe..380bef3358 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.5", + "version": "1.18.6", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index fdeab9b82b..ab795d5327 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.18.5", + "version": "1.18.6", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 093439705a..9859ba1798 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.18.5", + "version": "1.18.6", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index d011dbb473..80504ced29 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/server", - "version": "1.18.5", + "version": "1.18.6", "private": true, "type": "module", "license": "MIT", diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index 1fba117053..2d4bc6c65d 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/session-ui", - "version": "1.18.5", + "version": "1.18.6", "private": true, "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index e4fc453450..33e8109358 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.18.5", + "version": "1.18.6", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json index b14e49e2d8..d070a6a96e 100644 --- a/packages/stats/app/package.json +++ b/packages/stats/app/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-app", - "version": "1.18.5", + "version": "1.18.6", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/core/package.json b/packages/stats/core/package.json index f621709185..09493362f9 100644 --- a/packages/stats/core/package.json +++ b/packages/stats/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-core", - "version": "1.18.5", + "version": "1.18.6", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/server/package.json b/packages/stats/server/package.json index e679540583..1d1607612a 100644 --- a/packages/stats/server/package.json +++ b/packages/stats/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-server", - "version": "1.18.5", + "version": "1.18.6", "private": true, "type": "module", "license": "MIT", diff --git a/packages/tui/package.json b/packages/tui/package.json index d450262d85..f86400885b 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/tui", - "version": "1.18.5", + "version": "1.18.6", "private": true, "type": "module", "license": "MIT", diff --git a/packages/ui/package.json b/packages/ui/package.json index 971c3c89d0..3174e2113a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.18.5", + "version": "1.18.6", "type": "module", "license": "MIT", "repository": { diff --git a/packages/web/package.json b/packages/web/package.json index 2918b28f39..0ac92292d5 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.18.5", + "version": "1.18.6", "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 4e89e1f080..55684f7ab3 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.18.5", + "version": "1.18.6", "publisher": "sst-dev", "repository": { "type": "git", From 759f6afa800d6c0a91b84b552ee292f0a15085b2 Mon Sep 17 00:00:00 2001 From: David Siewert Date: Mon, 27 Jul 2026 08:48:01 +0600 Subject: [PATCH 014/129] fix(app): add scroll to project selector dropdown (#39016) Co-authored-by: Brendan Allan --- .../components/prompt-project-selector.tsx | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/packages/app/src/components/prompt-project-selector.tsx b/packages/app/src/components/prompt-project-selector.tsx index 1e5445517d..e47058f6a7 100644 --- a/packages/app/src/components/prompt-project-selector.tsx +++ b/packages/app/src/components/prompt-project-selector.tsx @@ -364,41 +364,43 @@ export function PromptProjectSelector(props: { - 1} - fallback={ - - - {(project) => ( - - )} - - - } - > - - props.controller.projects().some((project) => project.server?.key === server!.key), - )} +

+ 1} + fallback={ + + + {(project) => ( + + )} + + + } > - {(server) => ( -
-
- {server!.name} + + props.controller.projects().some((project) => project.server?.key === server!.key), + )} + > + {(server) => ( +
+
+ {server!.name} +
+ + project.server?.key === server!.key)}> + {(project) => ( + + )} + +
- - project.server?.key === server!.key)}> - {(project) => ( - - )} - - -
- )} - - + )} + + +
From 7ffc22c0ef6aba89fcf0e9de3a58e78a983c1dac Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 27 Jul 2026 02:49:22 +0000 Subject: [PATCH 015/129] chore: generate --- packages/app/src/components/prompt-project-selector.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/prompt-project-selector.tsx b/packages/app/src/components/prompt-project-selector.tsx index e47058f6a7..c110965961 100644 --- a/packages/app/src/components/prompt-project-selector.tsx +++ b/packages/app/src/components/prompt-project-selector.tsx @@ -390,7 +390,9 @@ export function PromptProjectSelector(props: { {server!.name}
- project.server?.key === server!.key)}> + project.server?.key === server!.key)} + > {(project) => ( )} From 9ca2b44235390b8e62fbe5c1a2ccceffbfd4bb9a Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:05:21 +0800 Subject: [PATCH 016/129] Connect provider e2e test (#39039) --- .../user-story/model-selection-flow.spec.ts | 101 ++++++++++++++++++ packages/app/e2e/utils/mock-server.ts | 23 +++- .../components/dialog-connect-provider.tsx | 3 +- .../components/dialog-select-directory-v2.tsx | 1 + .../components/dialog-select-directory.tsx | 4 +- .../dialog-select-model-unpaid-v2.tsx | 3 +- .../app/src/components/prompt-input-v2.tsx | 2 + 7 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 packages/app/e2e/user-story/model-selection-flow.spec.ts diff --git a/packages/app/e2e/user-story/model-selection-flow.spec.ts b/packages/app/e2e/user-story/model-selection-flow.spec.ts new file mode 100644 index 0000000000..2a220245e9 --- /dev/null +++ b/packages/app/e2e/user-story/model-selection-flow.spec.ts @@ -0,0 +1,101 @@ +import { expect, test } from "@playwright/test" +import { mockOpenCodeServer } from "../utils/mock-server" +import { expectAppVisible } from "../utils/waits" + +const directory = "C:/OpenCode/NewProject" + +test("creates a session in a new project, connects OpenCode Go, and selects its model", async ({ page }) => { + let connectedGo = false + let pendingGo = false + const connections: Array<{ integrationID: string; body: unknown }> = [] + + await mockOpenCodeServer(page, { + directory, + project: { + id: "proj_model_selection_flow", + worktree: directory, + vcs: "git", + name: "NewProject", + time: { created: 1_700_000_000_000, updated: 1_700_000_000_000 }, + sandboxes: [], + }, + provider: () => ({ + all: [ + { + id: "opencode", + name: "OpenCode", + models: { + "free-model": { + id: "free-model", + name: "Free Model", + cost: { input: 0, output: 0 }, + limit: { context: 200_000 }, + }, + }, + }, + { + id: "opencode-go", + name: "OpenCode Go", + models: { + "go-model-1": { + id: "go-model-1", + name: "Go Model 1", + cost: { input: 1, output: 1 }, + limit: { context: 200_000 }, + }, + }, + }, + ], + connected: connectedGo ? ["opencode", "opencode-go"] : ["opencode"], + default: { providerID: "opencode", modelID: "free-model" }, + }), + integrationMethods: { "opencode-go": [{ type: "api", label: "API key" }] }, + onConnectKey: (input) => { + connections.push(input) + if (input.integrationID === "opencode-go") pendingGo = true + }, + onInstanceDispose: () => { + if (pendingGo) connectedGo = true + }, + sessions: [], + pageMessages: () => ({ items: [] }), + fileList: (path) => + path + ? [] + : [{ name: "NewProject", path: "NewProject", absolute: directory, type: "directory", ignored: false }], + findFiles: () => ["NewProject"], + }) + await page.addInitScript(() => { + localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) + localStorage.setItem("opencode.global.dat:server", JSON.stringify({ projects: { local: [] } })) + }) + + await page.goto("/") + const addProject = page.locator('[data-action="home-add-project-row"]') + await expectAppVisible(addProject) + await addProject.click() + await page.locator("[data-directory-path]").click() + + await page.locator('[data-action="home-new-session"]').click() + await expectAppVisible(page.locator('[data-component="prompt-input-v2"]')) + + const modelControl = page.locator('[data-action="prompt-model"]') + await modelControl.click() + await expect(page.locator('[data-section="free-models"]')).toContainText("Free models provided by OpenCode") + + await page.locator('[data-provider-id="opencode-go"]').click() + await page.locator('[data-input="provider-api-key"]').fill("mock-go-api-key") + await page.locator('[data-action="provider-connect-submit"]').click() + await expect(page.locator('[data-component="dialog-v2"]')).toHaveCount(0) + expect(connections).toEqual([ + { integrationID: "opencode-go", body: { type: "api", key: "mock-go-api-key" } }, + ]) + + await expect(modelControl).toHaveAttribute("data-control-type", "popover") + await modelControl.click() + const goModel = page.locator('[data-option-key="opencode-go:go-model-1"]') + await expect(goModel).toBeVisible() + await goModel.click() + + await expect(modelControl).toContainText("Go Model 1") +}) diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts index 0e7dfc087c..76987421b6 100644 --- a/packages/app/e2e/utils/mock-server.ts +++ b/packages/app/e2e/utils/mock-server.ts @@ -5,7 +5,10 @@ const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mc export interface MockServerConfig { protocol?: "v1" | "v2" - provider: unknown + provider: unknown | (() => unknown) + integrationMethods?: Record + onConnectKey?: (input: { integrationID: string; body: unknown }) => void + onInstanceDispose?: () => void directory: string project: unknown sessions: ({ id: string } & Record)[] @@ -31,7 +34,6 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { const cursors = new Map() let nextCursor = 0 const staticRoutes: Record = { - "/provider": config.provider, "/path": { state: config.directory, config: config.directory, @@ -75,6 +77,18 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { if (path === "/api/health" && config.protocol === "v2") return json(route, { healthy: true, version: "2.0.0", pid: 1 }) if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: true }) + if (path === "/provider") + return json(route, typeof config.provider === "function" ? config.provider() : config.provider) + if (path === "/provider/auth") return json(route, config.integrationMethods ?? {}) + const legacyAuth = path.match(/^\/auth\/([^/]+)$/)?.[1] + if (legacyAuth && route.request().method() === "PUT") { + config.onConnectKey?.({ integrationID: legacyAuth, body: route.request().postDataJSON() }) + return json(route, true) + } + if (path === "/instance/dispose" && route.request().method() === "POST") { + config.onInstanceDispose?.() + return json(route, true) + } if (path === "/permission") return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? [])) if (path === "/question") @@ -130,8 +144,11 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { location: location(config), data: { id: integration, name: integration, methods: [{ type: "key", label: "API key" }], connections: [] }, }) - if (/^\/api\/integration\/[^/]+\/connect\/key$/.test(path) && route.request().method() === "POST") + const integrationConnect = path.match(/^\/api\/integration\/([^/]+)\/connect\/key$/)?.[1] + if (integrationConnect && route.request().method() === "POST") { + config.onConnectKey?.({ integrationID: integrationConnect, body: route.request().postDataJSON() }) return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) + } if (path === "/api/project") return json(route, [config.project]) if (path === "/api/project/current") return json(route, { id: (config.project as { id?: string }).id, directory: config.directory }) diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 744bc79227..19aa8ce89e 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -857,6 +857,7 @@ function ProviderConnection(props: { ref={apiKey} class="!w-full" name="apiKey" + data-input="provider-api-key" placeholder={language.t("provider.connect.apiKey.placeholder")} value={formStore.value} invalid={formStore.error !== undefined} @@ -873,7 +874,7 @@ function ProviderConnection(props: {
)} - + {language.t("common.continue")} diff --git a/packages/app/src/components/dialog-select-directory-v2.tsx b/packages/app/src/components/dialog-select-directory-v2.tsx index 847239d098..f3376ad35f 100644 --- a/packages/app/src/components/dialog-select-directory-v2.tsx +++ b/packages/app/src/components/dialog-select-directory-v2.tsx @@ -329,6 +329,7 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) { {(suggestion, index) => ( + )} onClose={restoreFocus} - > - - - - - {props.controls.model.selection.current()?.name ?? - language.t("dialog.model.select.title")} - - - + /> diff --git a/packages/app/src/components/prompt-project-selector.tsx b/packages/app/src/components/prompt-project-selector.tsx index c110965961..b84afdb56b 100644 --- a/packages/app/src/components/prompt-project-selector.tsx +++ b/packages/app/src/components/prompt-project-selector.tsx @@ -18,6 +18,7 @@ import { useLanguage } from "@/context/language" import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers" import { pathKey } from "@/utils/path-key" import { handleDocumentSearchKeydown } from "@/utils/search-keydown" +import { createMenuDismissController } from "@/utils/menu-dismiss-controller" export type PromptProject = { name?: string @@ -197,8 +198,8 @@ export function PromptProjectSelector(props: { }) { const [triggerReady, setTriggerReady] = createSignal(false) let contentRef: HTMLDivElement | undefined + const dismiss = createMenuDismissController(() => contentRef) let triggerFrame: number | undefined - let restoreTrigger = true // Floating UI requires a connected anchor; route transitions can construct this trigger before adoption. const setTriggerRef = (element: HTMLButtonElement) => { @@ -221,25 +222,15 @@ export function PromptProjectSelector(props: { props.controller.active() ? contentRef?.querySelector(`[data-option-key="${CSS.escape(props.controller.active())}"]`) : undefined - const afterClose = (callback: () => void) => { - const complete = () => { - if (contentRef?.isConnected) { - requestAnimationFrame(complete) - return - } - requestAnimationFrame(() => requestAnimationFrame(callback)) - } - requestAnimationFrame(complete) - } const selectProject = (project: PromptProject) => { - restoreTrigger = false + dismiss.preventTriggerRestore() props.controller.setOpen(false) - afterClose(() => props.controller.select(project)) + dismiss.afterClose(() => props.controller.select(project)) } const selectAction = (server?: string) => { - restoreTrigger = false + dismiss.preventTriggerRestore() props.controller.setOpen(false) - afterClose(() => props.controller.add(server)) + dismiss.afterClose(() => props.controller.add(server)) } const selectActive = () => { const project = props.controller.activeProject() @@ -267,7 +258,7 @@ export function PromptProjectSelector(props: { ) .filter((element) => !contentRef?.contains(element) && !element.hasAttribute("data-focus-trap")) .findLast((element) => element.offsetParent !== null) - restoreTrigger = false + dismiss.preventTriggerRestore() target?.focus() queueMicrotask(() => { if (props.controller.open()) props.controller.setOpen(false) @@ -291,7 +282,10 @@ export function PromptProjectSelector(props: { placement={props.placement ?? "bottom"} gutter={4} modal={false} - onOpenChange={(open) => props.controller.setOpen(open)} + onOpenChange={(open) => { + if (open) dismiss.allowTriggerRestore() + props.controller.setOpen(open) + }} > @@ -300,11 +294,9 @@ export function PromptProjectSelector(props: { id="prompt-project-menu" class="w-[243px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none [&[data-closed]]:!animate-none" onOpenAutoFocus={(event) => event.preventDefault()} - onPointerDownOutside={() => (restoreTrigger = false)} - onFocusOutside={() => (restoreTrigger = false)} - onCloseAutoFocus={(event) => { - if (!restoreTrigger) event.preventDefault() - }} + onPointerDownOutside={dismiss.preventTriggerRestore} + onFocusOutside={dismiss.preventTriggerRestore} + onCloseAutoFocus={dismiss.onCloseAutoFocus} >
diff --git a/packages/app/src/utils/menu-dismiss-controller.ts b/packages/app/src/utils/menu-dismiss-controller.ts new file mode 100644 index 0000000000..0a3009eb71 --- /dev/null +++ b/packages/app/src/utils/menu-dismiss-controller.ts @@ -0,0 +1,30 @@ +/** Coordinates focus restoration and actions that must run after menu content unmounts. */ +export function createMenuDismissController(content: () => HTMLElement | undefined) { + let restoreTrigger = true + + return { + /** Allows the menu primitive to restore focus to its trigger when closing. */ + allowTriggerRestore() { + restoreTrigger = true + }, + /** Keeps focus at its current or next destination instead of returning it to the trigger. */ + preventTriggerRestore() { + restoreTrigger = false + }, + /** Applies the current restoration policy during the menu primitive's close-focus event. */ + onCloseAutoFocus(event: Event) { + if (!restoreTrigger) event.preventDefault() + }, + /** Runs an action after the menu unmounts and its focus-close work has settled. */ + afterClose(callback: () => void) { + const complete = () => { + if (content()?.isConnected) { + requestAnimationFrame(complete) + return + } + requestAnimationFrame(() => requestAnimationFrame(callback)) + } + requestAnimationFrame(complete) + }, + } +} diff --git a/packages/storybook/.storybook/mocks/app/components/dialog-select-model.tsx b/packages/storybook/.storybook/mocks/app/components/dialog-select-model.tsx index 0c7ff8bbdd..0741f7e4ef 100644 --- a/packages/storybook/.storybook/mocks/app/components/dialog-select-model.tsx +++ b/packages/storybook/.storybook/mocks/app/components/dialog-select-model.tsx @@ -1,9 +1,8 @@ -import { splitProps } from "solid-js" +import { splitProps, type JSX } from "solid-js" -export function ModelSelectorPopover(props: { triggerAs: any; triggerProps?: Record; children: any }) { - const [local] = splitProps(props, ["triggerAs", "triggerProps", "children"]) - const Trigger = local.triggerAs - return {local.children} +export function ModelSelectorPopover(props: { trigger: (props: Record) => JSX.Element }) { + const [local] = splitProps(props, ["trigger"]) + return <>{local.trigger({})} } export const ModelSelectorPopoverV2 = ModelSelectorPopover From 3cc70160deb0eda7f67fbf5b0c0780000f5c342d Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 28 Jul 2026 01:43:55 +0000 Subject: [PATCH 041/129] chore: generate --- packages/app/src/components/dialog-select-model.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index e323c6f99c..9066f72434 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -1,13 +1,5 @@ import { Popover as Kobalte } from "@kobalte/core/popover" -import { - Component, - ComponentProps, - createEffect, - createMemo, - For, - JSX, - Show, -} from "solid-js" +import { Component, ComponentProps, createEffect, createMemo, For, JSX, Show } from "solid-js" import { createStore } from "solid-js/store" import { useLocal } from "@/context/local" import { useDialog } from "@opencode-ai/ui/context/dialog" From 3f9dad3fd0d4ce01ccb443896bce93d9e7f390eb Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 28 Jul 2026 01:57:02 +0000 Subject: [PATCH 042/129] 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 db0259a67e..06650c6488 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-2suqkFAUeT2YgppKqP14s0C+GE3TR3uNKOOD8vWEiAg=", - "aarch64-linux": "sha256-cJ/XYTodYmgs9ZFQhHQbfZPqxjIMmEvz062OsX6/6p4=", - "aarch64-darwin": "sha256-tKyqHeI6r1aHEk4vBS39+eA6wh9KcIwsa2ctcktKkEc=", - "x86_64-darwin": "sha256-1u7+dNjRcqKSvUTCEarVJ9fMrRUyg9y+4seZAm6Fmus=" + "x86_64-linux": "sha256-szhy0258K6IMWi3WRrAzRfhEEIDkGTS04KQwQw+DXyI=", + "aarch64-linux": "sha256-eVqUhXGKgx7rmRzpdr9G59Xrxvj2J0Dhx4Q7QRBHKiQ=", + "aarch64-darwin": "sha256-r/xAkD/yktfxOVTAArWYb0ZMWe/hEqh80dkYsigRkm8=", + "x86_64-darwin": "sha256-xEKSQZM4gYoz9RoaHEvuxrT8AFXHx8hMD1oKGi8lc3Y=" } } From 237e694df02830a586db9846c39e3f2a6e0d3c3f Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 03:56:40 +0000 Subject: [PATCH 043/129] fix(app): expand Windows file tree folders (#39249) Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> --- .../open-file-expand-folder.spec.ts | 132 ++++++++++++++++++ packages/app/src/context/file/path.test.ts | 23 +++ packages/app/src/context/file/path.ts | 7 +- 3 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 packages/app/e2e/regression/open-file-expand-folder.spec.ts diff --git a/packages/app/e2e/regression/open-file-expand-folder.spec.ts b/packages/app/e2e/regression/open-file-expand-folder.spec.ts new file mode 100644 index 0000000000..37739d2fbb --- /dev/null +++ b/packages/app/e2e/regression/open-file-expand-folder.spec.ts @@ -0,0 +1,132 @@ +import { base64Encode } from "@opencode-ai/core/util/encode" +import { expect, test } from "@playwright/test" +import { mockOpenCodeServer } from "../utils/mock-server" +import { expectSessionTitle } from "../utils/waits" + +const directory = "C:/OpenCode/OpenFileExpand" +const projectID = "proj_open_file_expand" +const sessionID = "ses_open_file_expand" +const title = "Open file expand" +const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}` + +test.use({ viewport: { width: 1440, height: 900 } }) + +test("expands a folder whose path has a trailing Windows separator", async ({ page }) => { + await mockOpenCodeServer(page, { + directory, + project: { + id: projectID, + worktree: directory, + vcs: "git", + name: "open-file-expand", + time: { created: 1700000000000, updated: 1700000000000 }, + sandboxes: [], + }, + provider: { + all: [ + { + id: "opencode", + name: "OpenCode", + models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } }, + }, + ], + connected: ["opencode"], + default: { providerID: "opencode", modelID: "test" }, + }, + sessions: [ + { + id: sessionID, + slug: sessionID, + projectID, + directory, + title, + version: "dev", + time: { created: 1700000000000, updated: 1700000000000 }, + }, + ], + vcsDiff: [], + fileList: (path) => { + if (path === "frontend\\" || path === "frontend") { + return [ + { + name: "app.ts", + path: "frontend\\app.ts", + absolute: `${directory}/frontend/app.ts`, + type: "file" as const, + ignored: false, + }, + ] + } + if (path) return [] + return [ + { + name: "frontend", + path: "frontend\\", + absolute: `${directory}/frontend`, + type: "directory" as const, + ignored: false, + }, + { + name: "README.md", + path: "README.md", + absolute: `${directory}/README.md`, + type: "file" as const, + ignored: false, + }, + ] + }, + fileContent: (path) => ({ type: "text", content: `contents:${path}` }), + pageMessages: () => ({ items: [] }), + }) + + await page.addInitScript( + ({ directory, server, sessionID }) => { + localStorage.setItem( + "settings.v3", + JSON.stringify({ general: { newLayoutDesigns: true, shouldDisplayTabsToast: false } }), + ) + localStorage.setItem( + "opencode.global.dat:server", + JSON.stringify({ + projects: { local: [{ worktree: directory, expanded: true }] }, + lastProject: { local: directory }, + }), + ) + localStorage.setItem( + "opencode.global.dat:layout", + JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }), + ) + localStorage.setItem( + "opencode.global.dat:review-panel-v2", + JSON.stringify({ sidebarOpened: true, sidebarWidth: 240, expandMode: "collapse" }), + ) + localStorage.setItem( + "opencode.window.browser.dat:tabs", + JSON.stringify([{ type: "session", server, sessionId: sessionID }]), + ) + }, + { directory, server, sessionID }, + ) + + await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`) + await expectSessionTitle(page, title) + + const panel = page.locator("#review-panel") + await panel.getByRole("button", { name: "Open file" }).click() + await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "") + + const sidebar = panel.locator('[data-component="session-review-v2-sidebar-root"]') + await expect(sidebar).toBeVisible() + + const frontendRow = panel.locator('[data-slot="file-tree-v2-row"][data-path="frontend"]') + await expect(frontendRow).toBeVisible() + await expect(frontendRow).toHaveAttribute("aria-expanded", "false") + await frontendRow.click() + await expect(frontendRow).toHaveAttribute("aria-expanded", "true") + + const appRow = panel.locator('[data-slot="file-tree-v2-row"][data-path="frontend/app.ts"]') + await expect(appRow).toBeVisible() + await appRow.click() + await expect(panel.getByRole("tab", { name: "app.ts" })).toHaveAttribute("data-selected", "") + await expect(panel.getByText("contents:frontend/app.ts", { exact: true })).toBeVisible() +}) diff --git a/packages/app/src/context/file/path.test.ts b/packages/app/src/context/file/path.test.ts index feef6d466e..99dd88ae0e 100644 --- a/packages/app/src/context/file/path.test.ts +++ b/packages/app/src/context/file/path.test.ts @@ -21,6 +21,29 @@ describe("file path helpers", () => { expect(path.normalize("c:\\repo\\src\\app.ts")).toBe("src\\app.ts") }) + test("normalizes Windows directory separators", () => { + const path = createPathHelpers(() => "C:\\repo") + expect(path.normalizeDir("frontend\\")).toBe("frontend") + expect(path.normalizeDir("frontend\\src\\")).toBe("frontend/src") + expect(path.normalizeDir("C:\\repo\\frontend\\")).toBe("frontend") + }) + + test("normalizes separators for Windows roots written with forward slashes", () => { + const path = createPathHelpers(() => "C:/repo") + expect(path.normalizeDir("frontend\\src\\")).toBe("frontend/src") + }) + + test("normalizes separators for Windows UNC roots", () => { + const path = createPathHelpers(() => "\\\\server\\share") + expect(path.normalizeDir("\\\\server\\share\\frontend\\")).toBe("frontend") + }) + + test("preserves backslashes in POSIX directory names", () => { + const path = createPathHelpers(() => "/repo") + expect(path.normalizeDir("literal\\name\\")).toBe("literal\\name\\") + expect(path.normalizeDir("literal\\name/")).toBe("literal\\name") + }) + test("keeps query/hash stripping behavior stable", () => { expect(stripQueryAndHash("a/b.ts#L12?x=1")).toBe("a/b.ts") expect(stripQueryAndHash("a/b.ts?x=1#L12")).toBe("a/b.ts") diff --git a/packages/app/src/context/file/path.ts b/packages/app/src/context/file/path.ts index 53f072b6cb..2bc4bde5e9 100644 --- a/packages/app/src/context/file/path.ts +++ b/packages/app/src/context/file/path.ts @@ -140,7 +140,12 @@ export function createPathHelpers(scope: () => string) { return normalize(tabValue) } - const normalizeDir = (input: string) => normalize(input).replace(/\/+$/, "") + const normalizeDir = (input: string) => { + const path = normalize(input) + const root = scope() + const windows = /^[A-Za-z]:/.test(root) || root.startsWith("\\\\") + return (windows ? path.replace(/\\/g, "/") : path).replace(/\/+$/, "") + } return { normalize, From 87db7a7f0af47035c8b533f7da41c2999415dc0b Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:59:30 +0800 Subject: [PATCH 044/129] fix(app): follow visual tab order (#39241) --- .../regression/tab-navigate-mousedown.spec.ts | 34 +++- .../app/src/components/titlebar-tab-nav.tsx | 150 +++++++++--------- .../src/components/titlebar-tab-order.test.ts | 23 +++ .../app/src/components/titlebar-tab-order.ts | 12 ++ .../app/src/components/titlebar-tab-strip.tsx | 139 ++++++++++++---- packages/app/src/components/titlebar.tsx | 34 ---- 6 files changed, 248 insertions(+), 144 deletions(-) create mode 100644 packages/app/src/components/titlebar-tab-order.test.ts create mode 100644 packages/app/src/components/titlebar-tab-order.ts diff --git a/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts b/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts index ae61b2acbf..b969b590d8 100644 --- a/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts +++ b/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts @@ -5,6 +5,8 @@ import { currentSession } from "../utils/mock-server" const server = "http://127.0.0.1:4096" const sessionA = session("ses_tab_a", "Tab A session") const sessionB = session("ses_tab_b", "Tab B session") +const sessionC = session("ses_tab_c", "Tab C session") +const unresolvedSessionID = "ses_tab_unresolved" test("pressing mouse down on a tab navigates before mouse up", async ({ page }) => { await mockServer(page) @@ -40,6 +42,34 @@ test("pressing mouse down on a tab navigates before mouse up", async ({ page }) await expect(page).toHaveURL(new RegExp(`${hrefB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`)) }) +test("keyboard navigation follows the visible tab order", async ({ page }) => { + await mockServer(page) + await page.addInitScript( + ({ server, sessionA, unresolved, sessionC }) => { + localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) + localStorage.setItem( + "opencode.window.browser.dat:tabs", + JSON.stringify([ + { type: "session", server, sessionId: sessionA }, + { type: "session", server, sessionId: unresolved }, + { type: "session", server, sessionId: sessionC }, + ]), + ) + }, + { server, sessionA: sessionA.id, unresolved: unresolvedSessionID, sessionC: sessionC.id }, + ) + + const hrefA = `/server/${base64Encode(server)}/session/${sessionA.id}` + const hrefC = `/server/${base64Encode(server)}/session/${sessionC.id}` + await page.goto(hrefA) + await expect(page.locator("[data-titlebar-tab-slot]:visible")).toHaveCount(2) + await expect(page.locator(`[data-titlebar-tab-slot]:has(a[href="${hrefC}"])`)).toBeVisible() + + await page.keyboard.press("Control+Alt+ArrowRight") + + await expect(page).toHaveURL(new RegExp(`${hrefC.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`)) +}) + function session(id: string, title: string) { return { id, @@ -53,10 +83,12 @@ function session(id: string, title: string) { } async function mockServer(page: Page) { - const sessions = [sessionA, sessionB] + const sessions = [sessionA, sessionB, sessionC] await page.route("**/*", async (route) => { const url = new URL(route.request().url()) if (url.origin !== server) return route.fallback() + if ([`/api/session/${unresolvedSessionID}`, `/session/${unresolvedSessionID}`].includes(url.pathname)) + return new Promise(() => {}) if (url.pathname === "/global/event" || url.pathname === "/event" || url.pathname === "/api/event") return sse(route) if (url.pathname === "/global/health") return json(route, { healthy: true }) diff --git a/packages/app/src/components/titlebar-tab-nav.tsx b/packages/app/src/components/titlebar-tab-nav.tsx index ceb43d4486..35f600c9f6 100644 --- a/packages/app/src/components/titlebar-tab-nav.tsx +++ b/packages/app/src/components/titlebar-tab-nav.tsx @@ -195,88 +195,86 @@ export function TabNavItem(props: { closeTab(event) }} > - - { + { + event.preventDefault() + event.stopPropagation() + }} + onMouseDown={(event) => { + // Navigate on mousedown to shave the press-release delay off tab switches. + if (event.button !== 0) return + if (editing()) return + if (props.suppressNavigation?.()) return + props.onNavigate() + }} + onClick={(event) => { + event.preventDefault() + // Mouse navigation already happened on mousedown; detail 0 means keyboard activation. + if (event.detail > 0) return + if (editing()) return + if (props.suppressNavigation?.()) return + props.onNavigate() + }} + class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base group-data-[editing='true']:text-v2-text-text-base [-webkit-user-drag:none]" + > + + + + { + titleEl = el + titleEl.textContent = title() ?? "" + }} + data-slot="tab-title" + data-titlebar-tab-title + class="min-w-0 flex-1 outline-none leading-4" + classList={{ + "overflow-hidden text-clip whitespace-nowrap": !editing(), + "select-text": editing(), + }} + contenteditable={editing() ? true : undefined} + onDblClick={openRename} + onKeyDown={(event) => { + event.stopPropagation() + if (event.key === "Enter") { + event.preventDefault() + void closeRename(true) + return + } + if (event.key !== "Escape") return event.preventDefault() + titleEl.textContent = props.session()?.title ?? "" + void closeRename(false) + }} + onBlur={() => void closeRename(true)} + onPointerDown={(event) => { + if (!editing()) return event.stopPropagation() }} - onMouseDown={(event) => { - // Navigate on mousedown to shave the press-release delay off tab switches. - if (event.button !== 0) return - if (editing()) return - if (props.suppressNavigation?.()) return - props.onNavigate() - }} onClick={(event) => { + if (!editing()) return event.preventDefault() - // Mouse navigation already happened on mousedown; detail 0 means keyboard activation. - if (event.detail > 0) return - if (editing()) return - if (props.suppressNavigation?.()) return - props.onNavigate() }} - class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base group-data-[editing='true']:text-v2-text-text-base [-webkit-user-drag:none]" - > - - - - { - titleEl = el - titleEl.textContent = title() ?? "" - }} - data-slot="tab-title" - data-titlebar-tab-title - class="min-w-0 flex-1 outline-none leading-4" - classList={{ - "overflow-hidden text-clip whitespace-nowrap": !editing(), - "select-text": editing(), - }} - contenteditable={editing() ? true : undefined} - onDblClick={openRename} - onKeyDown={(event) => { - event.stopPropagation() - if (event.key === "Enter") { - event.preventDefault() - void closeRename(true) - return - } - if (event.key !== "Escape") return - event.preventDefault() - titleEl.textContent = props.session()?.title ?? "" - void closeRename(false) - }} - onBlur={() => void closeRename(true)} - onPointerDown={(event) => { - if (!editing()) return - event.stopPropagation() - }} - onClick={(event) => { - if (!editing()) return - event.preventDefault() - }} - /> - - + /> +
{ + test("follows the visible left-to-right order", () => { + expect(adjacentTabKey(["c", "a", "b"], "c", 1)).toBe("a") + expect(adjacentTabKey(["c", "a", "b"], "a", -1)).toBe("c") + }) + + test("skips tabs omitted from the visible order", () => { + expect(adjacentTabKey(["a", "c"], "a", 1)).toBe("c") + expect(adjacentTabKey(["a", "c"], "c", 1)).toBe("a") + }) +}) + +test("merges reordered visible tabs around hidden tabs", () => { + expect(mergeVisibleTabOrder(["a", "hidden", "b", "c"], ["a", "b", "c"], ["c", "a", "b"])).toEqual([ + "c", + "hidden", + "a", + "b", + ]) +}) diff --git a/packages/app/src/components/titlebar-tab-order.ts b/packages/app/src/components/titlebar-tab-order.ts new file mode 100644 index 0000000000..a906b1fb32 --- /dev/null +++ b/packages/app/src/components/titlebar-tab-order.ts @@ -0,0 +1,12 @@ +export function adjacentTabKey(order: string[], current: string | undefined, offset: -1 | 1) { + if (!current || order.length === 0) return + const index = order.indexOf(current) + if (index === -1) return + return order[(index + offset + order.length) % order.length] +} + +export function mergeVisibleTabOrder(all: string[], current: string[], next: string[]) { + const visible = new Set(current) + const reordered = next.values() + return all.map((key) => (visible.has(key) ? (reordered.next().value ?? key) : key)) +} diff --git a/packages/app/src/components/titlebar-tab-strip.tsx b/packages/app/src/components/titlebar-tab-strip.tsx index 9dc44a65cd..0a4b4da12d 100644 --- a/packages/app/src/components/titlebar-tab-strip.tsx +++ b/packages/app/src/components/titlebar-tab-strip.tsx @@ -1,4 +1,5 @@ -import { createEffect, createMemo, createResource, createRoot, For, onCleanup, onMount } from "solid-js" +import { createEffect, createMemo, createResource, createRoot, For, onCleanup, onMount, Show } from "solid-js" +import { createStore } from "solid-js/store" import { createResizeObserver } from "@solid-primitives/resize-observer" import { DragDropProvider, PointerSensor } from "@dnd-kit/solid" import { isSortable, useSortable } from "@dnd-kit/solid/sortable" @@ -17,6 +18,8 @@ import { createTabPromptState } from "@/context/prompt" import { base64Encode } from "@opencode-ai/core/util/encode" import { showToast } from "@/utils/toast" import { canStartTabDrag, isTabCloseTarget } from "./titlebar-tab-gesture" +import { adjacentTabKey, mergeVisibleTabOrder } from "./titlebar-tab-order" +import type { Session } from "@opencode-ai/sdk/v2" function SessionTabSlot(props: { tab: SessionTab @@ -24,12 +27,12 @@ function SessionTabSlot(props: { index: () => number active: () => boolean forceTruncate: boolean - serverCtx: () => ServerCtx | undefined + session: () => Session | undefined + fallbackTitle?: string + onRename: (title: string) => Promise onNavigate: (element: HTMLDivElement) => void onClose: () => void }) { - const tabs = useTabs() - const language = useLanguage() const sortable = useSortable({ get id() { return props.id @@ -39,6 +42,47 @@ function SessionTabSlot(props: { }, }) let ref!: HTMLDivElement + + return ( +
+ { + ref = el + }} + href={tabHref(props.tab)} + server={props.tab.server} + session={props.session} + fallbackTitle={props.fallbackTitle} + onRename={props.onRename} + onNavigate={() => props.onNavigate(ref)} + onClose={props.onClose} + active={props.active()} + forceTruncate={props.forceTruncate} + dragging={sortable.isDragSource()} + /> +
+ ) +} + +function SessionTabEntry(props: { + tab: SessionTab + id: string + index: () => number + active: () => boolean + forceTruncate: boolean + serverCtx: () => ServerCtx | undefined + onVisibleChange: (visible: boolean) => void + onNavigate: (element: HTMLDivElement) => void + onClose: () => void +}) { + const tabs = useTabs() + const language = useLanguage() const sdk = createMemo(() => props.serverCtx()?.sdk ?? null) const cachedSession = createMemo(() => props.serverCtx()?.sync.session.peek(props.tab.sessionId)) const persisted = createMemo(() => tabs.info[props.id]) @@ -51,6 +95,7 @@ function SessionTabSlot(props: { ) const session = createMemo(() => cachedSession() ?? loadedSession()) const missingSession = createMemo(() => !!props.serverCtx() && !loadedSession.loading && !session()) + const visible = createMemo(() => !!session() || missingSession() || !!persisted()?.title) let prefetched = false const rename = async (title: string) => { @@ -72,6 +117,8 @@ function SessionTabSlot(props: { } } + createEffect(() => props.onVisibleChange(visible())) + createEffect(() => { const ctx = props.serverCtx() const value = session() @@ -103,30 +150,20 @@ function SessionTabSlot(props: { }) return ( -
- { - ref = el - }} - href={tabHref(props.tab)} - server={props.tab.server} + + props.onNavigate(ref)} + onNavigate={props.onNavigate} onClose={props.onClose} - active={props.active()} - forceTruncate={props.forceTruncate} - dragging={sortable.isDragSource()} /> -
+ ) } @@ -183,11 +220,39 @@ export function TitlebarTabStrip(props: { }) { const global = useGlobal() const language = useLanguage() + const command = useCommand() let scrollRef!: HTMLDivElement let listRef!: HTMLDivElement let resizeFrame: number | undefined + const [visibility, setVisibility] = createStore>({}) + const visibleTabs = createMemo(() => props.tabs.filter((tab) => tab.type === "draft" || visibility[tabKey(tab)])) + const visibleTabIds = () => visibleTabs().map(tabKey) - const tabIds = () => props.tabs.map(tabKey) + command.register("titlebar-tab-cycle", () => [ + { + id: `tab.prev`, + category: "tab", + title: "", + keybind: `mod+option+ArrowLeft,ctrl+shift+tab`, + hidden: true, + onSelect: () => selectAdjacentTab(-1), + }, + { + id: `tab.next`, + category: "tab", + title: "", + keybind: `mod+option+ArrowRight,ctrl+tab`, + hidden: true, + onSelect: () => selectAdjacentTab(1), + }, + ]) + + function selectAdjacentTab(offset: -1 | 1) { + const current = props.currentTab() + const key = adjacentTabKey(visibleTabIds(), current ? tabKey(current) : undefined, offset) + const next = props.tabs.find((tab) => tabKey(tab) === key) + if (next) props.onNavigate(next) + } function refreshOverflow() { if (!scrollRef) return @@ -215,7 +280,7 @@ export function TitlebarTabStrip(props: { createEffect(() => { props.tabs.length - tabIds() + visibleTabIds() refreshOverflow() }) @@ -251,22 +316,29 @@ export function TitlebarTabStrip(props: { props.onNavigate(tab, tabEl ?? undefined) }} onDragEnd={(event) => { - const current = tabIds() + const current = visibleTabIds() const source = event.operation.source if (event.canceled || !isSortable(source)) return const { initialIndex, index } = source if (initialIndex !== index) { - props.onReorder(arrayMove(current, source.initialIndex, source.index)) + props.onReorder( + mergeVisibleTabOrder( + props.tabs.map(tabKey), + current, + arrayMove(current, source.initialIndex, source.index), + ), + ) } }} >
- {(tab, index) => { + {(tab) => { const id = tabKey(tab) let ref!: HTMLDivElement - useTabShortcut(index, () => props.onNavigate(tab, ref)) + const visibleIndex = () => visibleTabs().findIndex((item) => tabKey(item) === id) + useTabShortcut(visibleIndex, () => props.onNavigate(tab, ref)) const serverCtx = createMemo(() => { if (tab.type !== "session") return const conn = global.servers.list().find((item) => ServerConnection.key(item) === tab.server) @@ -275,13 +347,14 @@ export function TitlebarTabStrip(props: { if (tab.type === "session") { return ( - props.currentTab() === tab} forceTruncate={props.forceTruncate} serverCtx={serverCtx} + onVisibleChange={(visible) => setVisibility(id, visible)} onNavigate={(element) => { ref = element props.onNavigate(tab, element) @@ -295,7 +368,7 @@ export function TitlebarTabStrip(props: { props.currentTab() === tab} title={language.t("command.session.new")} onNavigate={(element) => { @@ -329,7 +402,7 @@ function useTabShortcut(index: () => number, onSelect: () => void) { command.register(() => { const number = index() + 1 - if (number > 9) return [] + if (number < 1 || number > 9) return [] return [ { id: `tab.${number}`, diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 41d134f0f4..32d7e80b2f 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -338,40 +338,6 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl keybind: "mod+shift+t", onSelect: () => tabsStoreActions.reopenClosedTab(), }, - { - id: `tab.prev`, - category: "tab", - title: "", - keybind: `mod+option+ArrowLeft,ctrl+shift+tab`, - hidden: true, - onSelect: () => { - let index = tabsStore.findIndex((tab) => tab === currentTab()) - if (index === -1) return - - index -= 1 - if (index === -1) index = tabsStore.length - 1 - - const next = tabsStore[index] - if (next) tabs.select(next) - }, - }, - { - id: `tab.next`, - category: "tab", - title: "", - keybind: `mod+option+ArrowRight,ctrl+tab`, - hidden: true, - onSelect: () => { - let index = tabsStore.findIndex((tab) => tab === currentTab()) - if (index === -1) return - - index += 1 - if (index === tabsStore.length) index = 0 - - const next = tabsStore[index] - if (next) tabs.select(next) - }, - }, ].filter((v) => v !== undefined) }) From 9c8060d96db2ed9fa6b45f8b19f8ec2aefadf2eb Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:18:27 +0800 Subject: [PATCH 045/129] refactor(app): extract keybind settings controller (#39226) --- .../app/src/components/settings-keybinds.tsx | 350 +++++++++++++++--- .../test-browser/settings-keybinds.test.ts | 107 ++++++ 2 files changed, 396 insertions(+), 61 deletions(-) create mode 100644 packages/app/test-browser/settings-keybinds.test.ts diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx index cdec8b435c..ce74be4586 100644 --- a/packages/app/src/components/settings-keybinds.tsx +++ b/packages/app/src/components/settings-keybinds.tsx @@ -30,6 +30,8 @@ type KeybindMeta = { type KeybindMap = Record type CommandContext = ReturnType +type LanguageContext = ReturnType +type SettingsContext = ReturnType const GROUPS: KeybindGroup[] = ["General", "Session", "Navigation", "Model and agent", "Terminal", "Prompt"] @@ -122,7 +124,7 @@ function keybinds(value: unknown): KeybindMap { return value as KeybindMap } -function listFor(command: CommandContext, map: KeybindMap, palette: string) { +function listFor(command: Pick, map: KeybindMap, palette: string) { const out = new Map() out.set(PALETTE_ID, { title: palette, group: "General" }) @@ -262,7 +264,274 @@ function useKeyCapture(input: { }) } +export function createKeybindSettingsController( + input: { + command: Pick + settings: { + current: { keybinds: unknown } + keybinds: Pick + } + target?: Document + notify?: (toast: { title: string; description: string }) => void + }, + language: Pick = useLanguage(), +) { + const [store, setStore] = createStore({ active: null as string | null }) + const overrides = createMemo(() => keybinds(input.settings.current.keybinds)) + const list = createMemo(() => { + language.locale() + return listFor(input.command, overrides(), language.t("command.palette")) + }) + const grouped = createMemo(() => groupedFor(list())) + const title = (id: string) => list().get(id)?.title ?? "" + const effective = (id: string) => { + if (id === PALETTE_ID) return input.settings.keybinds.get(id) ?? DEFAULT_PALETTE_KEYBIND + + const custom = input.settings.keybinds.get(id) + if (typeof custom === "string") return custom + + const live = input.command.options.find((item) => item.id === id) + if (live?.keybind) return live.keybind + return input.command.catalog.find((item) => item.id === id)?.keybind + } + const used = createMemo(() => { + const value = new Map() + + for (const id of list().keys()) { + for (const signature of signatures(effective(id))) { + const items = value.get(signature) + if (items) { + items.push({ id, title: title(id) }) + continue + } + value.set(signature, [{ id, title: title(id) }]) + } + } + + return value + }) + const stop = () => { + if (!store.active) return + setStore("active", null) + input.command.keybinds(true) + } + const toggle = (id: string) => { + if (store.active === id) { + stop() + return + } + if (store.active) stop() + setStore("active", id) + input.command.keybinds(false) + } + const notify = input.notify ?? ((toast: { title: string; description: string }) => showToast(toast)) + + const handle = (event: KeyboardEvent) => { + const id = store.active + if (!id) return + + event.preventDefault() + event.stopPropagation() + event.stopImmediatePropagation() + + if (event.key === "Escape") { + stop() + return + } + + const clear = + (event.key === "Backspace" || event.key === "Delete") && + !event.ctrlKey && + !event.metaKey && + !event.altKey && + !event.shiftKey + if (clear) { + input.settings.keybinds.set(id, "none") + stop() + return + } + + const next = recordKeybind(event) + if (!next) return + + const conflicts = new Map() + for (const signature of signatures(next)) { + for (const item of used().get(signature) ?? []) { + if (item.id === id) continue + conflicts.set(item.id, item.title) + } + } + + if (conflicts.size > 0) { + notify({ + title: language.t("settings.shortcuts.conflict.title"), + description: language.t("settings.shortcuts.conflict.description", { + keybind: formatKeybind(next, language.t), + titles: [...conflicts.values()].join(", "), + }), + }) + return + } + + input.settings.keybinds.set(id, next) + stop() + } + + const target = input.target ?? (typeof document === "object" ? document : undefined) + if (target) makeEventListener(target, "keydown", handle, { capture: true }) + + onCleanup(() => { + if (store.active) input.command.keybinds(true) + }) + + return { + catalog: { + groups: GROUPS, + filtered: (query: string) => + filteredFor(query, list(), grouped(), (id) => formatKeybind(effective(id) ?? "", language.t)), + title, + keybind: (id: string) => formatKeybind(effective(id) ?? "", language.t), + }, + capture: { + active: () => store.active, + toggle, + }, + settings: { + hasOverrides: () => Object.values(overrides()).some((value) => typeof value === "string"), + reset: () => { + stop() + input.settings.keybinds.resetAll() + notify({ + title: language.t("settings.shortcuts.reset.toast.title"), + description: language.t("settings.shortcuts.reset.toast.description"), + }) + }, + }, + } +} + +function SettingsKeybindsV2() { + const command = useCommand() + const settings = useSettings() + const controller = createKeybindSettingsController({ + command, + settings, + }) + + return ( + + ) +} + +function SettingsKeybindsV2View(props: { + groups: KeybindGroup[] + filtered: (query: string) => Map + title: (id: string) => string + keybind: (id: string) => string + active: () => string | null + onCapture: (id: string) => void + hasOverrides: () => boolean + onReset: () => void +}) { + const language = useLanguage() + const [store, setStore] = createStore({ filter: "" }) + const filtered = createMemo(() => props.filtered(store.filter)) + const hasResults = createMemo(() => props.groups.some((group) => (filtered().get(group)?.length ?? 0) > 0)) + + return ( + <> +
+
+

{language.t("settings.shortcuts.title")}

+ + {language.t("settings.shortcuts.reset.button")} + +
+ +
+
+
+ + {(group) => ( + 0}> +
+

{language.t(groupKey[group])}

+ + + {(id) => ( +
+ {props.title(id)} + +
+ )} +
+
+
+
+ )} +
+ +
+ {language.t("settings.shortcuts.search.empty")} + "{store.filter}" +
+
+
+
+ + ) +} + export const SettingsKeybinds: Component<{ v2?: boolean }> = (props) => { + if (props.v2) return + const command = useCommand() const language = useLanguage() const settings = useSettings() @@ -476,78 +745,37 @@ export const SettingsKeybinds: Component<{ v2?: boolean }> = (props) => { ) return ( - -
-
-
-

{language.t("settings.shortcuts.title")}

- -
- -
- - setStore("filter", v)} - placeholder={language.t("settings.shortcuts.search.placeholder")} - spellcheck={false} - autocorrect="off" - autocomplete="off" - autocapitalize="off" - class="flex-1" - /> - - setStore("filter", "")} /> - -
-
-
- {groups} -
- } - > - <> -
-
-

{language.t("settings.shortcuts.title")}

- +
+
+
+
+

{language.t("settings.shortcuts.title")}

+
-
-
{groups}
- - +
+ {groups} +
) } diff --git a/packages/app/test-browser/settings-keybinds.test.ts b/packages/app/test-browser/settings-keybinds.test.ts new file mode 100644 index 0000000000..75f0b75543 --- /dev/null +++ b/packages/app/test-browser/settings-keybinds.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, test } from "bun:test" +import { createRoot } from "solid-js" +import { createKeybindSettingsController } from "../src/components/settings-keybinds" + +function setup(overrides: Record = {}) { + const changes: [string, string][] = [] + const suppression: boolean[] = [] + const notifications: { title: string; description?: string }[] = [] + let resets = 0 + let controller: ReturnType + + const dispose = createRoot((dispose) => { + controller = createKeybindSettingsController( + { + command: { + catalog: [ + { id: "session.alpha", title: "Alpha", keybind: "mod+a" }, + { id: "session.beta", title: "Beta", keybind: "mod+b" }, + ], + options: [], + keybinds: (enabled) => suppression.push(enabled), + }, + settings: { + current: { keybinds: overrides }, + keybinds: { + get: (id) => overrides[id], + set: (id, value) => { + overrides[id] = value + changes.push([id, value]) + }, + resetAll: () => { + resets++ + }, + }, + }, + notify: (toast) => notifications.push(toast), + }, + { + locale: () => "en", + t: (key, params) => { + if (params) return `${key}:${Object.values(params).join("|")}` + if (key === "common.key.alt") return "Alt" + return String(key) + }, + }, + ) + return dispose + }) + + return { + controller: controller!, + changes, + suppression, + notifications, + resets: () => resets, + dispose, + } +} + +function modKey(key: string) { + const mac = /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) + return new KeyboardEvent("keydown", { key, ctrlKey: !mac, metaKey: mac, bubbles: true, cancelable: true }) +} + +describe("keybind settings controller", () => { + test("derives the catalog, effective bindings, and filtered groups", () => { + const state = setup({ "session.beta": "alt+k" }) + + expect(state.controller.catalog.title("session.alpha")).toBe("Alpha") + expect(state.controller.catalog.keybind("session.beta")).toBe("Alt+K") + expect(state.controller.catalog.filtered("alt k").get("Session")).toEqual(["session.beta"]) + expect(state.controller.settings.hasOverrides()).toBe(true) + + state.dispose() + }) + + test("captures bindings, rejects conflicts, and restores command handling", () => { + const state = setup() + + state.controller.capture.toggle("session.beta") + document.dispatchEvent(modKey("a")) + expect(state.changes).toEqual([]) + expect(state.notifications).toHaveLength(1) + expect(state.controller.capture.active()).toBe("session.beta") + + document.dispatchEvent(modKey("x")) + expect(state.changes).toEqual([["session.beta", "mod+x"]]) + expect(state.suppression).toEqual([false, true]) + expect(state.controller.capture.active()).toBeNull() + + state.controller.capture.toggle("session.alpha") + state.dispose() + expect(state.suppression).toEqual([false, true, false, true]) + document.dispatchEvent(modKey("z")) + expect(state.changes).toEqual([["session.beta", "mod+x"]]) + }) + + test("resets persisted overrides and reports success", () => { + const state = setup({ "session.alpha": "none" }) + + state.controller.settings.reset() + expect(state.resets()).toBe(1) + expect(state.notifications[0]?.title).toBe("settings.shortcuts.reset.toast.title") + + state.dispose() + }) +}) From 921b1c6a34651870db4585e4e4114f0ba87102a7 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 27 Jul 2026 23:19:33 -0500 Subject: [PATCH 046/129] feat(mcp): upgrade client SDK to v2 (#39247) Co-authored-by: Matt Carey --- bun.lock | 82 +-- package.json | 1 - packages/opencode/package.json | 3 +- packages/opencode/src/cli/cmd/mcp.ts | 148 ++--- packages/opencode/src/mcp/auth.ts | 4 + packages/opencode/src/mcp/catalog.ts | 82 +-- packages/opencode/src/mcp/index.ts | 75 ++- packages/opencode/src/mcp/oauth-callback.ts | 12 +- packages/opencode/src/mcp/oauth-provider.ts | 85 ++- .../routes/instance/httpapi/groups/mcp.ts | 1 + .../routes/instance/httpapi/handlers/mcp.ts | 2 +- packages/opencode/src/tool/code-mode.ts | 3 +- .../test/fixture/mcp-lifecycle-stdio.ts | 7 +- .../test/fixture/mcp-session-recovery.ts | 4 +- packages/opencode/test/mcp/catalog.test.ts | 22 +- packages/opencode/test/mcp/headers.test.ts | 25 +- packages/opencode/test/mcp/lifecycle.test.ts | 31 +- .../test/mcp/oauth-auto-connect.test.ts | 8 +- .../opencode/test/mcp/oauth-browser.test.ts | 6 +- .../opencode/test/mcp/oauth-callback.test.ts | 2 +- .../opencode/test/mcp/oauth-provider.test.ts | 41 -- .../test/mcp/session-recovery.test.ts | 2 +- .../test/tool/code-mode-integration.test.ts | 17 +- packages/opencode/test/tool/code-mode.test.ts | 2 +- packages/opencode/test/tool/registry.test.ts | 2 +- packages/sdk/js/src/v2/gen/sdk.gen.ts | 2 + packages/sdk/js/src/v2/gen/types.gen.ts | 1 + .../@modelcontextprotocol%2Fsdk@1.29.0.patch | 629 ------------------ 28 files changed, 271 insertions(+), 1028 deletions(-) delete mode 100644 patches/@modelcontextprotocol%2Fsdk@1.29.0.patch diff --git a/bun.lock b/bun.lock index 39749a43ca..f5df0e1fad 100644 --- a/bun.lock +++ b/bun.lock @@ -592,7 +592,7 @@ "@effect/platform-node": "catalog:", "@ff-labs/fff-bun": "0.9.4", "@gitlab/opencode-gitlab-auth": "1.3.3", - "@modelcontextprotocol/sdk": "1.29.0", + "@modelcontextprotocol/client": "2.0.0-beta.5", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", @@ -666,6 +666,7 @@ }, "devDependencies": { "@babel/core": "7.28.4", + "@modelcontextprotocol/server": "2.0.0-beta.5", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/core": "workspace:*", "@opencode-ai/http-recorder": "workspace:*", @@ -1072,7 +1073,6 @@ "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", - "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", @@ -1674,8 +1674,6 @@ "@hey-api/types": ["@hey-api/types@0.1.2", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="], - "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], - "@hono/standard-validator": ["@hono/standard-validator@0.2.0", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-pFq0UVAnjzXcDAgqFpDeVL3MOUPrlIh/kPqBDvbCYoThVhhS+Vf37VcdsakdOFFGiqoiYVxp3LifXFhGhp/rgQ=="], "@ibm/plex": ["@ibm/plex@6.4.1", "", { "dependencies": { "@ibm/telemetry-js": "^1.5.1" } }, "sha512-fnsipQywHt3zWvsnlyYKMikcVI7E2fEwpiPnIHFqlbByXVfQfANAAeJk1IV4mNnxhppUIDlhU0TzwYwL++Rn2g=="], @@ -1830,7 +1828,11 @@ "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + "@modelcontextprotocol/client": ["@modelcontextprotocol/client@2.0.0-beta.5", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0-beta.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "jose": "^6.1.3", "pkce-challenge": "^5.0.0", "zod": "^4.2.0" } }, "sha512-YuuNm5f2TMoFQRje1UqVP8TJRjijCXMz4ckvoVpx1cUXuBEmykWQ2d8R536pek6UKcXT41T5nWc4qR1JFIbEmg=="], + + "@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.0.0-beta.5", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-HKbY9XTbsDy1Y6r2I55TGE3JEapM0vg96e1MUmBIF9LGjos5gjhcIrTz1yvBPLg2aFKHjwhUAQfRdrCEnPxNew=="], + + "@modelcontextprotocol/server": ["@modelcontextprotocol/server@2.0.0-beta.5", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0-beta.5", "zod": "^4.2.0" } }, "sha512-i1E5l75rQKsgY/AKAIspgMBH1vEL7dqiK7tHr0L+raYcb0SWOziqNGJXGIG6NY4AlXDWIKGJQGB7Nqfs3oUi5g=="], "@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="], @@ -3390,7 +3392,7 @@ "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -3402,8 +3404,6 @@ "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], - "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], - "crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="], "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], @@ -3708,8 +3708,6 @@ "express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], - "express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="], - "expressive-code": ["expressive-code@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "@expressive-code/plugin-frames": "^0.41.7", "@expressive-code/plugin-shiki": "^0.41.7", "@expressive-code/plugin-text-markers": "^0.41.7" } }, "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA=="], "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], @@ -4094,8 +4092,6 @@ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], @@ -5004,8 +5000,6 @@ "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -5954,15 +5948,13 @@ "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "@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=="], + "@modelcontextprotocol/client/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], - "@modelcontextprotocol/sdk/hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="], + "@modelcontextprotocol/client/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "@modelcontextprotocol/sdk/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + "@modelcontextprotocol/core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - - "@modelcontextprotocol/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@modelcontextprotocol/server/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], @@ -6238,6 +6230,8 @@ "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + "body-parser/content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], @@ -6320,6 +6314,8 @@ "execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + "express/content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -6468,8 +6464,6 @@ "roarr/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], - "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], - "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], @@ -6798,58 +6792,28 @@ "@malept/flatpak-bundler/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "@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=="], - - "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], - - "@modelcontextprotocol/sdk/express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - - "@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "@modelcontextprotocol/sdk/express/send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - - "@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - - "@modelcontextprotocol/sdk/express/type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], - "@octokit/auth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-app/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], - "@octokit/auth-app/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/auth-app/@octokit/request-error/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], "@octokit/auth-oauth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-oauth-app/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], - "@octokit/auth-oauth-app/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/auth-oauth-app/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/auth-oauth-device/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-oauth-device/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], - "@octokit/auth-oauth-device/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/auth-oauth-device/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/auth-oauth-user/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-oauth-user/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], - "@octokit/auth-oauth-user/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/auth-oauth-user/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], @@ -6862,14 +6826,10 @@ "@octokit/graphql/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], - "@octokit/graphql/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/graphql/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="], "@octokit/oauth-methods/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], - "@octokit/oauth-methods/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/oauth-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/plugin-paginate-rest/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], @@ -7378,10 +7338,6 @@ "@jsx-email/cli/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@modelcontextprotocol/sdk/express/type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - - "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - "@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/auth-app/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], @@ -7390,20 +7346,14 @@ "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], - "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/plugin-paginate-rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], - "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], - "@octokit/rest/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@opencode-ai/core/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], diff --git a/package.json b/package.json index 5fd0f1d51a..176c7a2459 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,6 @@ "pacote@21.5.0": "patches/pacote@21.5.0.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", - "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch" } diff --git a/packages/opencode/package.json b/packages/opencode/package.json index aa09e4610c..6544ba09e2 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -30,6 +30,7 @@ }, "devDependencies": { "@babel/core": "7.28.4", + "@modelcontextprotocol/server": "2.0.0-beta.5", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/core": "workspace:*", "@opencode-ai/http-recorder": "workspace:*", @@ -80,7 +81,7 @@ "@effect/platform-node": "catalog:", "@ff-labs/fff-bun": "0.9.4", "@gitlab/opencode-gitlab-auth": "1.3.3", - "@modelcontextprotocol/sdk": "1.29.0", + "@modelcontextprotocol/client": "2.0.0-beta.5", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index c2d2ee2f3b..009b6ba755 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -2,13 +2,10 @@ import { cmd } from "./cmd" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { effectCmd } from "../effect-cmd" import { Cause } from "effect" -import { Client } from "@modelcontextprotocol/sdk/client/index.js" -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" -import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js" -import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js" +import { Client, StreamableHTTPClientTransport, UnauthorizedError } from "@modelcontextprotocol/client" import * as prompts from "@clack/prompts" import { UI } from "../ui" -import { MCP } from "../../mcp" +import { CLIENT_OPTIONS, MCP } from "../../mcp" import { McpAuth } from "../../mcp/auth" import { McpOAuthProvider } from "../../mcp/oauth-provider" import { Config } from "@/config/config" @@ -731,107 +728,52 @@ export const McpDebugCommand = effectCmd({ const spinner = prompts.spinner() spinner.start("Testing connection...") - // Test basic HTTP connectivity first - try { - const response = await fetch(serverConfig.url, { - method: "POST", - headers: { - ...serverConfig.headers, - "Content-Type": "application/json", - Accept: "application/json, text/event-stream", + const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined + let authorizationUrl: URL | undefined + const authProvider = new McpOAuthProvider( + serverName, + serverConfig.url, + { + clientId: oauthConfig?.clientId, + clientSecret: oauthConfig?.clientSecret, + scope: oauthConfig?.scope, + redirectUri: oauthConfig?.redirectUri, + }, + { + onRedirect: async (url) => { + authorizationUrl = url }, - body: JSON.stringify({ - jsonrpc: "2.0", - method: "initialize", - params: { - protocolVersion: LATEST_PROTOCOL_VERSION, - capabilities: {}, - clientInfo: { name: "opencode-debug", version: InstallationVersion }, - }, - id: 1, - }), - }) + }, + auth, + ) + const transport = new StreamableHTTPClientTransport(new URL(serverConfig.url), { + authProvider, + requestInit: serverConfig.headers ? { headers: serverConfig.headers } : undefined, + }) + const client = new Client({ name: "opencode-debug", version: InstallationVersion }, CLIENT_OPTIONS) - spinner.stop(`HTTP response: ${response.status} ${response.statusText}`) - - // Check for WWW-Authenticate header - const wwwAuth = response.headers.get("www-authenticate") - if (wwwAuth) { - prompts.log.info(`WWW-Authenticate: ${wwwAuth}`) - } - - if (response.status === 401) { - prompts.log.info("Initial unauthenticated check returned 401, so this server requires OAuth") - - // Try to discover OAuth metadata - const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined - const authProvider = new McpOAuthProvider( - serverName, - serverConfig.url, - { - clientId: oauthConfig?.clientId, - clientSecret: oauthConfig?.clientSecret, - scope: oauthConfig?.scope, - redirectUri: oauthConfig?.redirectUri, - }, - { - onRedirect: async () => {}, - }, - auth, - ) - - prompts.log.info("Testing OAuth flow (without completing authorization)...") - - // Try creating transport with auth provider to trigger discovery - const transport = new StreamableHTTPClientTransport(new URL(serverConfig.url), { - authProvider, - requestInit: serverConfig.headers ? { headers: serverConfig.headers } : undefined, - }) - - try { - const client = new Client({ - name: "opencode-debug", - version: InstallationVersion, - }) - await client.connect(transport) - prompts.log.success("Connection successful (already authenticated)") - await client.close() - } catch (error) { - if (error instanceof UnauthorizedError) { - prompts.log.info(`OAuth flow triggered: ${error.message}`) - - // Check if dynamic registration would be attempted - const clientInfo = await authProvider.clientInformation() - if (clientInfo) { - prompts.log.info(`Client ID available: ${clientInfo.client_id}`) - } else { - prompts.log.info("No client ID - dynamic registration will be attempted") - } - } else { - prompts.log.error(`Connection error: ${error instanceof Error ? error.message : String(error)}`) - } - } - } else if (response.status >= 200 && response.status < 300) { - prompts.log.success("Server responded successfully (no auth required or already authenticated)") - const body = await response.text() - try { - const json = JSON.parse(body) - if (json.result?.serverInfo) { - prompts.log.info(`Server info: ${JSON.stringify(json.result.serverInfo)}`) - } - } catch { - // Not JSON, ignore - } - } else { - prompts.log.warn(`Unexpected status: ${response.status}`) - const body = await response.text().catch(() => "") - if (body) { - prompts.log.info(`Response body: ${body.substring(0, 500)}`) - } - } + try { + await client.connect(transport) + spinner.stop("SDK connection successful") + prompts.log.success( + `Connected using MCP ${client.getNegotiatedProtocolVersion() ?? "unknown"} (${client.getProtocolEra() ?? "unknown"})`, + ) + const serverInfo = client.getServerVersion() + if (serverInfo) prompts.log.info(`Server info: ${JSON.stringify(serverInfo)}`) } catch (error) { - spinner.stop("Connection failed", 1) - prompts.log.error(`Error: ${error instanceof Error ? error.message : String(error)}`) + if (error instanceof UnauthorizedError) { + spinner.stop("OAuth required") + prompts.log.info(`OAuth flow triggered: ${error.message}`) + if (authorizationUrl) prompts.log.info(`Authorization URL: ${authorizationUrl}`) + const clientInfo = await authProvider.clientInformation() + if (clientInfo) prompts.log.info(`Client ID available: ${clientInfo.client_id}`) + if (!clientInfo) prompts.log.info("No client ID - dynamic registration will be attempted") + } else { + spinner.stop("Connection failed", 1) + prompts.log.error(`Error: ${error instanceof Error ? error.message : String(error)}`) + } + } finally { + await client.close().catch(() => {}) } prompts.outro("Debug complete") diff --git a/packages/opencode/src/mcp/auth.ts b/packages/opencode/src/mcp/auth.ts index 808aa30296..543b4fb0e3 100644 --- a/packages/opencode/src/mcp/auth.ts +++ b/packages/opencode/src/mcp/auth.ts @@ -11,6 +11,7 @@ export const Tokens = Schema.Struct({ refreshToken: Schema.mutableKey(Schema.optional(Schema.String)), expiresAt: Schema.mutableKey(Schema.optional(Schema.Number)), scope: Schema.mutableKey(Schema.optional(Schema.String)), + issuer: Schema.mutableKey(Schema.optional(Schema.String)), }) export type Tokens = Schema.Schema.Type @@ -19,6 +20,9 @@ export const ClientInfo = Schema.Struct({ clientSecret: Schema.mutableKey(Schema.optional(Schema.String)), clientIdIssuedAt: Schema.mutableKey(Schema.optional(Schema.Number)), clientSecretExpiresAt: Schema.mutableKey(Schema.optional(Schema.Number)), + redirectUris: Schema.mutableKey(Schema.optional(Schema.Array(Schema.String))), + issuer: Schema.mutableKey(Schema.optional(Schema.String)), + configPreRegistered: Schema.mutableKey(Schema.optional(Schema.Boolean)), }) export type ClientInfo = Schema.Schema.Type diff --git a/packages/opencode/src/mcp/catalog.ts b/packages/opencode/src/mcp/catalog.ts index 3f985eeb94..113ddbd5b8 100644 --- a/packages/opencode/src/mcp/catalog.ts +++ b/packages/opencode/src/mcp/catalog.ts @@ -1,40 +1,8 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js" -import { - CallToolResultSchema, - ListToolsResultSchema, - ToolSchema, - type Tool as MCPToolDef, -} from "@modelcontextprotocol/sdk/types.js" +import { Client, type Tool as MCPToolDef } from "@modelcontextprotocol/client" import { dynamicTool, jsonSchema, type JSONSchema7, type Tool } from "ai" import { Effect } from "effect" const DEFAULT_TIMEOUT = 30_000 -const MAX_LIST_PAGES = 1_000 - -const TolerantListToolsResultSchema = ListToolsResultSchema.extend({ - tools: ToolSchema.omit({ outputSchema: true }).array(), -}) - -export async function paginate( - list: (cursor?: string) => Promise, - items: (result: R) => T[], -) { - const result: T[] = [] - const cursors = new Set() - let cursor: string | undefined - - for (let page = 0; page < MAX_LIST_PAGES; page++) { - const page = await list(cursor) - result.push(...items(page)) - if (page.nextCursor === undefined) return result - if (cursors.has(page.nextCursor)) throw new Error(`MCP list returned duplicate cursor: ${page.nextCursor}`) - cursors.add(page.nextCursor) - cursor = page.nextCursor - } - - throw new Error(`MCP list exceeded ${MAX_LIST_PAGES} pages`) -} - export function defs(client: Client, timeout?: number) { return listTools(client, timeout ?? DEFAULT_TIMEOUT).pipe(Effect.catch(() => Effect.void)) } @@ -56,7 +24,6 @@ export function convertTool(mcpTool: MCPToolDef, client: Client, timeout?: numbe name: mcpTool.name, arguments: (args || {}) as Record, }, - CallToolResultSchema, { resetTimeoutOnProgress: true, signal: options.abortSignal, @@ -118,53 +85,26 @@ export const sanitize = (value: string) => value.replace(/[^a-zA-Z0-9_-]/g, "_") export const toolName = (clientName: string, name: string) => sanitize(clientName) + "_" + sanitize(name) -export function prompts(client: Client, timeout?: number) { - if (!client.getServerCapabilities()?.prompts) return Promise.resolve([]) - return paginate( - (cursor) => client.listPrompts(cursor === undefined ? undefined : { cursor }, { timeout }), - (result) => result.prompts, - ) +export async function prompts(client: Client, timeout?: number) { + if (!client.getServerCapabilities()?.prompts) return [] + return (await client.listPrompts(undefined, { timeout })).prompts } -export function resources(client: Client, timeout?: number) { - if (!client.getServerCapabilities()?.resources) return Promise.resolve([]) - return paginate( - (cursor) => client.listResources(cursor === undefined ? undefined : { cursor }, { timeout }), - (result) => result.resources, - ) +export async function resources(client: Client, timeout?: number) { + if (!client.getServerCapabilities()?.resources) return [] + return (await client.listResources(undefined, { timeout })).resources } -export function resourceTemplates(client: Client, timeout?: number) { - if (!client.getServerCapabilities()?.resources) return Promise.resolve([]) - return paginate( - (cursor) => client.listResourceTemplates(cursor === undefined ? undefined : { cursor }, { timeout }), - (result) => result.resourceTemplates, - ) +export async function resourceTemplates(client: Client, timeout?: number) { + if (!client.getServerCapabilities()?.resources) return [] + return (await client.listResourceTemplates(undefined, { timeout })).resourceTemplates } function listTools(client: Client, timeout: number) { return Effect.tryPromise({ - try: () => - paginate( - async (cursor) => { - const params = cursor === undefined ? undefined : { cursor } - try { - return await client.listTools(params, { timeout }) - } catch (error) { - if (!(error instanceof Error) || !isOutputSchemaValidationError(error)) throw error - return client.request({ method: "tools/list", params }, TolerantListToolsResultSchema, { timeout }) - } - }, - (result) => result.tools, - ), + try: async () => (await client.listTools(undefined, { timeout })).tools, catch: (error) => (error instanceof Error ? error : new Error(String(error))), }) } -function isOutputSchemaValidationError(error: Error) { - return /can't resolve reference|resolves to more than one schema|outputSchema|schema.*reference|reference.*schema/i.test( - error.message, - ) -} - export * as McpCatalog from "./catalog" diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 05f12fa2ee..75d685b82a 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -3,18 +3,18 @@ import { pathToFileURL } from "node:url" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { serviceUse } from "@opencode-ai/core/effect/service-use" -import { Client, type ClientOptions } from "@modelcontextprotocol/sdk/client/index.js" -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" -import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js" import { - ListRootsRequestSchema, + Client, + type ClientOptions, + StreamableHTTPClientTransport, + SSEClientTransport, + UnauthorizedError, + RegistrationRejectedError, + SdkHttpError, type LoggingMessageNotification, - LoggingMessageNotificationSchema, type Tool as MCPToolDef, - ToolListChangedNotificationSchema, -} from "@modelcontextprotocol/sdk/types.js" +} from "@modelcontextprotocol/client" +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio" import { Config } from "@/config/config" import { ConfigMCPV1 } from "@opencode-ai/core/v1/config/mcp" import { NamedError } from "@opencode-ai/core/util/error" @@ -36,7 +36,7 @@ import { McpEvent } from "@opencode-ai/schema/mcp-event" import { McpBrowser } from "./browser" const DEFAULT_TIMEOUT = 30_000 -const CLIENT_OPTIONS = { +export const CLIENT_OPTIONS = { capabilities: { // https://github.com/anomalyco/opencode/issues/11948 // sampling: {}, @@ -47,6 +47,8 @@ const CLIENT_OPTIONS = { // https://github.com/anomalyco/opencode/issues/28567 // tasks: {}, }, + versionNegotiation: { mode: "auto" }, + listMaxPages: 1_000, } satisfies ClientOptions export const Resource = Schema.Struct({ @@ -70,13 +72,19 @@ export class NotFoundError extends Schema.TaggedErrorClass()("MCP name: Schema.String, }) {} -type MCPClient = Client +type MCPClient = Client & { onToolsChanged?: (error: Error | null) => void } function createClient(directory: string) { - const client = new Client({ name: "opencode", version: InstallationVersion }, CLIENT_OPTIONS) - client.setRequestHandler(ListRootsRequestSchema, () => - Promise.resolve({ roots: [{ uri: pathToFileURL(directory).href }] }), + const client: MCPClient = new Client( + { name: "opencode", version: InstallationVersion }, + { + ...CLIENT_OPTIONS, + listChanged: { + tools: { autoRefresh: false, onChanged: (error) => client.onToolsChanged?.(error) }, + }, + }, ) + client.setRequestHandler("roots/list", async () => ({ roots: [{ uri: pathToFileURL(directory).href }] })) return client } @@ -190,7 +198,11 @@ export interface Interface { mcpName: string, onAuthorization?: (authorizationUrl: string) => void, ) => Effect.Effect - readonly finishAuth: (mcpName: string, authorizationCode: string) => Effect.Effect + readonly finishAuth: ( + mcpName: string, + authorizationCode: string, + iss?: string, + ) => Effect.Effect readonly removeAuth: (mcpName: string) => Effect.Effect readonly supportsOAuth: (mcpName: string) => Effect.Effect readonly hasStoredTokens: (mcpName: string) => Effect.Effect @@ -291,11 +303,18 @@ const layer = Layer.effect( Effect.map((client) => ({ client, transportName: name })), Effect.catch((error) => { const lastError = error instanceof Error ? error : new Error(String(error)) + const registrationRejected = + error instanceof RegistrationRejectedError || + lastError.message.includes("registration") || + lastError.message.includes("client_id") const isAuthError = - error instanceof UnauthorizedError || (authProvider && lastError.message.includes("OAuth")) + error instanceof UnauthorizedError || + registrationRejected || + (authProvider && error instanceof SdkHttpError && error.status === 401) || + (authProvider && lastError.message.includes("OAuth")) if (isAuthError) { - if (lastError.message.includes("registration") || lastError.message.includes("client_id")) { + if (registrationRejected) { lastStatus = { status: "needs_client_registration" as const, error: "Server does not support dynamic client registration. Please provide clientId in config.", @@ -454,12 +473,16 @@ const layer = Layer.effect( ) } - client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => + client.setNotificationHandler("notifications/message", (notification) => bridge.promise(serverLog(name, notification.params)), ) if (!client.getServerCapabilities()?.tools) return - client.setNotificationHandler(ToolListChangedNotificationSchema, async () => { + client.onToolsChanged = async (error) => { + if (error) { + await bridge.promise(Effect.logWarning("failed to refresh MCP tools", { server: name, error: error.message })) + return + } if (s.clients[name] !== client || s.status[name]?.status !== "connected") return const listed = await bridge.promise(McpCatalog.defs(client, timeout)) @@ -468,7 +491,7 @@ const layer = Layer.effect( s.defs[name] = listed await bridge.promise(events.publish(ToolsChanged, { server: name }).pipe(Effect.ignore)) - }) + } } function serverLog(name: string, params: LoggingMessageNotification["params"]) { @@ -904,7 +927,7 @@ const layer = Layer.effect( }), ) - const code = yield* Effect.promise(() => callbackPromise) + const callback = yield* Effect.promise(() => callbackPromise) const storedState = yield* auth.getOAuthState(mcpName) if (storedState !== result.oauthState) { @@ -912,16 +935,20 @@ const layer = Layer.effect( throw new Error("OAuth state mismatch - potential CSRF attack") } yield* auth.clearOAuthState(mcpName) - return yield* finishAuth(mcpName, code) + return yield* finishAuth(mcpName, callback.code, callback.iss) }) - const finishAuth = Effect.fn("MCP.finishAuth")(function* (mcpName: string, authorizationCode: string) { + const finishAuth = Effect.fn("MCP.finishAuth")(function* ( + mcpName: string, + authorizationCode: string, + iss?: string, + ) { yield* requireMcpConfig(mcpName) const pending = pendingOAuthTransports.get(mcpName) if (!pending) throw new Error(`No pending OAuth flow for MCP server: ${mcpName}`) const error = yield* Effect.tryPromise({ - try: () => pending.transport.finishAuth(authorizationCode), + try: () => pending.transport.finishAuth(authorizationCode, iss), catch: (error) => error, }).pipe( Effect.match({ diff --git a/packages/opencode/src/mcp/oauth-callback.ts b/packages/opencode/src/mcp/oauth-callback.ts index 84007902b8..71f6ec9539 100644 --- a/packages/opencode/src/mcp/oauth-callback.ts +++ b/packages/opencode/src/mcp/oauth-callback.ts @@ -9,8 +9,13 @@ const OAUTH_CALLBACK_HOST = "127.0.0.1" let currentPort = OAUTH_CALLBACK_PORT let currentPath = OAUTH_CALLBACK_PATH +export interface AuthorizationCallback { + code: string + iss?: string +} + interface PendingAuth { - resolve: (code: string) => void + resolve: (callback: AuthorizationCallback) => void reject: (error: Error) => void timeout: ReturnType } @@ -49,6 +54,7 @@ function handleRequest(req: import("http").IncomingMessage, res: import("http"). } const code = url.searchParams.get("code") + const iss = url.searchParams.get("iss") ?? undefined const state = url.searchParams.get("state") const error = url.searchParams.get("error") const errorDescription = url.searchParams.get("error_description") @@ -95,7 +101,7 @@ function handleRequest(req: import("http").IncomingMessage, res: import("http"). clearTimeout(pending.timeout) pendingAuths.delete(state) cleanupStateIndex(state) - pending.resolve(code) + pending.resolve({ code, iss }) res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }) res.end(OauthCallbackPage.success({ provider: "MCP" })) @@ -130,7 +136,7 @@ export async function ensureRunning(redirectUri?: string): Promise { }) } -export function waitForCallback(oauthState: string, mcpName?: string): Promise { +export function waitForCallback(oauthState: string, mcpName?: string): Promise { if (mcpName) mcpNameToState.set(mcpName, oauthState) return new Promise((resolve, reject) => { const timeout = setTimeout(() => { diff --git a/packages/opencode/src/mcp/oauth-provider.ts b/packages/opencode/src/mcp/oauth-provider.ts index 596bfe1d55..a3f99a55f5 100644 --- a/packages/opencode/src/mcp/oauth-provider.ts +++ b/packages/opencode/src/mcp/oauth-provider.ts @@ -1,10 +1,9 @@ -import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js" import type { + OAuthClientProvider, OAuthClientMetadata, - OAuthTokens, - OAuthClientInformation, - OAuthClientInformationFull, -} from "@modelcontextprotocol/sdk/shared/auth.js" + StoredOAuthTokens, + StoredOAuthClientInformation, +} from "@modelcontextprotocol/client" import { Effect } from "effect" import { McpAuth } from "./auth" @@ -23,6 +22,14 @@ export interface McpOAuthCallbacks { onRedirect: (url: URL) => void | Promise } +function registrationMetadata(info: StoredOAuthClientInformation) { + return { + clientIdIssuedAt: "client_id_issued_at" in info ? info.client_id_issued_at : undefined, + clientSecretExpiresAt: "client_secret_expires_at" in info ? info.client_secret_expires_at : undefined, + redirectUris: "redirect_uris" in info ? info.redirect_uris : undefined, + } +} + export class McpOAuthProvider implements OAuthClientProvider { constructor( protected mcpName: string, @@ -52,18 +59,21 @@ export class McpOAuthProvider implements OAuthClientProvider { } } - async clientInformation(): Promise { + async clientInformation(): Promise { + const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl)) if (this.config.clientId) { + const issuer = entry?.clientInfo?.clientId === this.config.clientId ? entry.clientInfo.issuer : undefined return { client_id: this.config.clientId, client_secret: this.config.clientSecret, + ...(issuer !== undefined ? { issuer } : {}), } } // Check stored client info (from dynamic registration) // Use getForUrl to validate credentials are for the current server URL - const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl)) if (entry?.clientInfo) { + if (entry.clientInfo.configPreRegistered) return undefined // Check if client secret has expired if (entry.clientInfo.clientSecretExpiresAt && entry.clientInfo.clientSecretExpiresAt < Date.now() / 1000) { return undefined @@ -71,6 +81,14 @@ export class McpOAuthProvider implements OAuthClientProvider { return { client_id: entry.clientInfo.clientId, client_secret: entry.clientInfo.clientSecret, + ...(entry.clientInfo.clientIdIssuedAt !== undefined + ? { client_id_issued_at: entry.clientInfo.clientIdIssuedAt } + : {}), + ...(entry.clientInfo.clientSecretExpiresAt !== undefined + ? { client_secret_expires_at: entry.clientInfo.clientSecretExpiresAt } + : {}), + redirect_uris: entry.clientInfo.redirectUris ? [...entry.clientInfo.redirectUris] : [this.redirectUrl], + ...(entry.clientInfo.issuer !== undefined ? { issuer: entry.clientInfo.issuer } : {}), } } @@ -78,22 +96,36 @@ export class McpOAuthProvider implements OAuthClientProvider { return undefined } - async saveClientInformation(info: OAuthClientInformationFull): Promise { + async saveClientInformation(info: StoredOAuthClientInformation): Promise { + if (this.config.clientId && info.client_id === this.config.clientId) { + await Effect.runPromise( + this.auth.updateClientInfo( + this.mcpName, + { clientId: info.client_id, issuer: info.issuer, configPreRegistered: true }, + this.serverUrl, + ), + ) + return + } + + const metadata = registrationMetadata(info) await Effect.runPromise( this.auth.updateClientInfo( this.mcpName, { clientId: info.client_id, clientSecret: info.client_secret, - clientIdIssuedAt: info.client_id_issued_at, - clientSecretExpiresAt: info.client_secret_expires_at, + clientIdIssuedAt: metadata.clientIdIssuedAt, + clientSecretExpiresAt: metadata.clientSecretExpiresAt, + redirectUris: metadata.redirectUris ? [...metadata.redirectUris] : [this.redirectUrl], + issuer: info.issuer, }, this.serverUrl, ), ) } - async tokens(): Promise { + async tokens(): Promise { // Use getForUrl to validate tokens are for the current server URL const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl)) if (!entry?.tokens) return undefined @@ -106,18 +138,20 @@ export class McpOAuthProvider implements OAuthClientProvider { ? Math.max(0, Math.floor(entry.tokens.expiresAt - Date.now() / 1000)) : undefined, scope: entry.tokens.scope, + issuer: entry.tokens.issuer, } } - async saveTokens(tokens: OAuthTokens): Promise { + async saveTokens(tokens: StoredOAuthTokens): Promise { await Effect.runPromise( this.auth.updateTokens( this.mcpName, { accessToken: tokens.access_token, refreshToken: tokens.refresh_token, - expiresAt: tokens.expires_in ? Date.now() / 1000 + tokens.expires_in : undefined, + expiresAt: tokens.expires_in !== undefined ? Date.now() / 1000 + tokens.expires_in : undefined, scope: tokens.scope, + issuer: tokens.issuer, }, this.serverUrl, ), @@ -181,10 +215,10 @@ export class McpOAuthProvider implements OAuthClientProvider { } export class McpOAuthPendingProvider extends McpOAuthProvider { - private pendingClientInfo?: OAuthClientInformationFull - private pendingTokens?: OAuthTokens + private pendingClientInfo?: StoredOAuthClientInformation + private pendingTokens?: StoredOAuthTokens - override async clientInformation(): Promise { + override async clientInformation(): Promise { if (!this.config.clientId) return this.pendingClientInfo return { client_id: this.config.clientId, @@ -192,15 +226,15 @@ export class McpOAuthPendingProvider extends McpOAuthProvider { } } - override async saveClientInformation(info: OAuthClientInformationFull): Promise { + override async saveClientInformation(info: StoredOAuthClientInformation): Promise { this.pendingClientInfo = info } - override async tokens(): Promise { + override async tokens(): Promise { return this.pendingTokens } - override async saveTokens(tokens: OAuthTokens): Promise { + override async saveTokens(tokens: StoredOAuthTokens): Promise { this.pendingTokens = tokens } @@ -211,6 +245,7 @@ export class McpOAuthPendingProvider extends McpOAuthProvider { async commit(): Promise { if (!this.pendingTokens) return + const pendingMetadata = this.pendingClientInfo ? registrationMetadata(this.pendingClientInfo) : undefined await Effect.runPromise( this.auth.set( this.mcpName, @@ -218,16 +253,22 @@ export class McpOAuthPendingProvider extends McpOAuthProvider { tokens: { accessToken: this.pendingTokens.access_token, refreshToken: this.pendingTokens.refresh_token, - expiresAt: this.pendingTokens.expires_in ? Date.now() / 1000 + this.pendingTokens.expires_in : undefined, + expiresAt: + this.pendingTokens.expires_in !== undefined + ? Date.now() / 1000 + this.pendingTokens.expires_in + : undefined, scope: this.pendingTokens.scope, + issuer: this.pendingTokens.issuer, }, clientInfo: this.pendingClientInfo && !this.config.clientId ? { clientId: this.pendingClientInfo.client_id, clientSecret: this.pendingClientInfo.client_secret, - clientIdIssuedAt: this.pendingClientInfo.client_id_issued_at, - clientSecretExpiresAt: this.pendingClientInfo.client_secret_expires_at, + clientIdIssuedAt: pendingMetadata?.clientIdIssuedAt, + clientSecretExpiresAt: pendingMetadata?.clientSecretExpiresAt, + redirectUris: pendingMetadata?.redirectUris ? [...pendingMetadata.redirectUris] : [this.redirectUrl], + issuer: this.pendingClientInfo.issuer, } : undefined, }, diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/mcp.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/mcp.ts index a6fb064d73..ca56d10b4e 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/mcp.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/mcp.ts @@ -20,6 +20,7 @@ export const AuthStartResponse = Schema.Struct({ }) export const AuthCallbackPayload = Schema.Struct({ code: Schema.String, + iss: Schema.optional(Schema.String), }) export const AuthRemoveResponse = Schema.Struct({ success: Schema.Literal(true), diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/mcp.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/mcp.ts index cdf0cc1e70..3a367a84e7 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/mcp.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/mcp.ts @@ -38,7 +38,7 @@ export const mcpHandlers = HttpApiBuilder.group(InstanceHttpApi, "mcp", (handler payload: typeof AuthCallbackPayload.Type }) { return yield* mcp - .finishAuth(ctx.params.name, ctx.payload.code) + .finishAuth(ctx.params.name, ctx.payload.code, ctx.payload.iss) .pipe( Effect.catchTag("MCP.NotFoundError", (error) => Effect.fail( diff --git a/packages/opencode/src/tool/code-mode.ts b/packages/opencode/src/tool/code-mode.ts index 332d4b43f1..5d9b809a30 100644 --- a/packages/opencode/src/tool/code-mode.ts +++ b/packages/opencode/src/tool/code-mode.ts @@ -1,5 +1,5 @@ import * as Tool from "./tool" -import { CallToolResultSchema, type CallToolResult } from "@modelcontextprotocol/sdk/types.js" +import { type CallToolResult } from "@modelcontextprotocol/client" import { Cause, Effect, Schema } from "effect" import { CodeMode, Tool as SandboxTool, toolError } from "@opencode-ai/codemode" import { MCP } from "@/mcp" @@ -149,7 +149,6 @@ const invokeChildTool = Effect.fn("CodeMode.invokeChildTool")(function* (input: return yield* Effect.promise(async () => { const raw = await input.entry.tool.client.callTool( { name: input.entry.tool.def.name, arguments: input.args }, - CallToolResultSchema, { resetTimeoutOnProgress: true, signal: input.ctx.abort, diff --git a/packages/opencode/test/fixture/mcp-lifecycle-stdio.ts b/packages/opencode/test/fixture/mcp-lifecycle-stdio.ts index b01ed921cf..6260c7e3d4 100644 --- a/packages/opencode/test/fixture/mcp-lifecycle-stdio.ts +++ b/packages/opencode/test/fixture/mcp-lifecycle-stdio.ts @@ -1,6 +1,5 @@ -import { Server } from "@modelcontextprotocol/sdk/server/index.js" -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" -import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" +import { Server } from "@modelcontextprotocol/server" +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio" if (process.argv.includes("--hang")) { const pidFile = process.env.MCP_LIFECYCLE_PID_FILE @@ -11,7 +10,7 @@ if (process.argv.includes("--hang")) { const server = new Server({ name: "mcp-lifecycle-stdio", version: "1.0.0" }, { capabilities: { tools: {} } }) -server.setRequestHandler(ListToolsRequestSchema, () => +server.setRequestHandler("tools/list", () => Promise.resolve({ tools: [ { diff --git a/packages/opencode/test/fixture/mcp-session-recovery.ts b/packages/opencode/test/fixture/mcp-session-recovery.ts index c20fb5aa58..7e7e576f1c 100644 --- a/packages/opencode/test/fixture/mcp-session-recovery.ts +++ b/packages/opencode/test/fixture/mcp-session-recovery.ts @@ -1,6 +1,4 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js" -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" -import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js" +import { Client, LATEST_PROTOCOL_VERSION, StreamableHTTPClientTransport } from "@modelcontextprotocol/client" const posts: Array<{ method: string; session: string | null }> = [] let initializeCount = 0 diff --git a/packages/opencode/test/mcp/catalog.test.ts b/packages/opencode/test/mcp/catalog.test.ts index 7b0d6403bb..d1367df304 100644 --- a/packages/opencode/test/mcp/catalog.test.ts +++ b/packages/opencode/test/mcp/catalog.test.ts @@ -1,8 +1,6 @@ import { describe, expect, test } from "bun:test" -import { Client } from "@modelcontextprotocol/sdk/client/index.js" -import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js" -import { Server } from "@modelcontextprotocol/sdk/server/index.js" -import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" +import { Client, InMemoryTransport } from "@modelcontextprotocol/client" +import { Server } from "@modelcontextprotocol/server" import { McpCatalog } from "@/mcp/catalog" import { Effect } from "effect" @@ -52,16 +50,16 @@ describe("McpCatalog.convertTool", () => { test("preserves output schema validation across paginated tool discovery", async () => { const server = new Server({ name: "pagination", version: "1.0.0" }, { capabilities: { tools: {} } }) - server.setRequestHandler(ListToolsRequestSchema, ({ params }) => + server.setRequestHandler("tools/list", ({ params }) => Promise.resolve( params?.cursor === "page-2" ? { tools: [ { name: "second", - inputSchema: { type: "object" }, + inputSchema: { type: "object" as const }, outputSchema: { - type: "object", + type: "object" as const, properties: { value: { type: "number" } }, required: ["value"], }, @@ -72,9 +70,9 @@ test("preserves output schema validation across paginated tool discovery", async tools: [ { name: "first", - inputSchema: { type: "object" }, + inputSchema: { type: "object" as const }, outputSchema: { - type: "object", + type: "object" as const, properties: { value: { type: "string" } }, required: ["value"], }, @@ -84,7 +82,7 @@ test("preserves output schema validation across paginated tool discovery", async }, ), ) - server.setRequestHandler(CallToolRequestSchema, ({ params }) => + server.setRequestHandler("tools/call", ({ params }) => Promise.resolve({ content: [], structuredContent: { value: params.name === "first" ? 42 : 1 }, @@ -98,9 +96,7 @@ test("preserves output schema validation across paginated tool discovery", async try { const tools = await Effect.runPromise(McpCatalog.defs(client)) expect(tools?.map((tool) => tool.name)).toEqual(["first", "second"]) - await expect(client.callTool({ name: "first", arguments: {} })).rejects.toThrow( - "Structured content does not match the tool's output schema", - ) + await expect(client.callTool({ name: "first", arguments: {} })).rejects.toThrow(/output schema/i) } finally { await Promise.all([client.close(), server.close()]) } diff --git a/packages/opencode/test/mcp/headers.test.ts b/packages/opencode/test/mcp/headers.test.ts index 31cfc20d51..323aea2e47 100644 --- a/packages/opencode/test/mcp/headers.test.ts +++ b/packages/opencode/test/mcp/headers.test.ts @@ -1,7 +1,5 @@ import { describe, expect } from "bun:test" -import { Server } from "@modelcontextprotocol/sdk/server/index.js" -import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js" -import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" +import { Server, WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Effect } from "effect" import { testEffect } from "../lib/effect" @@ -13,7 +11,7 @@ const serve = Effect.acquireRelease( Effect.promise(async () => { const requests: Headers[] = [] const protocol = new Server({ name: "headers", version: "1.0.0" }, { capabilities: { tools: {} } }) - protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] })) + protocol.setRequestHandler("tools/list", () => Promise.resolve({ tools: [] })) const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID(), enableJsonResponse: true, @@ -38,6 +36,11 @@ const serve = Effect.acquireRelease( (server) => Effect.promise(server.close), ) +const serveUnauthorized = Effect.acquireRelease( + Effect.sync(() => Bun.serve({ port: 0, fetch: () => new Response("Unauthorized", { status: 401 }) })), + (server) => Effect.sync(() => server.stop(true)), +) + describe("mcp.headers", () => { it.instance("headers are passed to transports when oauth is enabled (default)", () => Effect.gen(function* () { @@ -99,4 +102,18 @@ describe("mcp.headers", () => { } }), ) + + it.instance("reports 401 as failed when oauth is explicitly disabled", () => + Effect.gen(function* () { + const server = yield* serveUnauthorized + const mcp = yield* MCP.Service + const result = yield* mcp.add("unauthorized-no-oauth", { + type: "remote", + url: server.url.toString(), + oauth: false, + }) + + expect(result.status).toMatchObject({ "unauthorized-no-oauth": { status: "failed" } }) + }), + ) }) diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index 80c8fd22f8..b001898779 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -1,18 +1,12 @@ import path from "node:path" import { pathToFileURL } from "node:url" import { expect } from "bun:test" -import { Server } from "@modelcontextprotocol/sdk/server/index.js" -import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js" import { - GetPromptRequestSchema, - ListPromptsRequestSchema, - ListResourcesRequestSchema, - ListResourceTemplatesRequestSchema, - ListToolsRequestSchema, - ReadResourceRequestSchema, + Server, + WebStandardStreamableHTTPServerTransport, type ServerCapabilities, type Tool, -} from "@modelcontextprotocol/sdk/types.js" +} from "@modelcontextprotocol/server" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Cause, Effect, Exit } from "effect" import type { MCP as MCPNS } from "../../src/mcp/index" @@ -66,35 +60,35 @@ function lifecycleServer(input?: { capabilities?: ServerCapabilities; instructio }) if (capabilities.tools) { - protocol.setRequestHandler(ListToolsRequestSchema, (request) => { + protocol.setRequestHandler("tools/list", (request) => { if (state.listToolsError) throw new Error(state.listToolsError) const page = state.toolPages?.[request.params?.cursor ?? "initial"] return Promise.resolve({ tools: page?.items ?? state.tools, nextCursor: page?.nextCursor }) }) } if (capabilities.prompts) { - protocol.setRequestHandler(ListPromptsRequestSchema, (request) => { + protocol.setRequestHandler("prompts/list", (request) => { const page = state.promptPages?.[request.params?.cursor ?? "initial"] return Promise.resolve({ prompts: page?.items ?? state.prompts, nextCursor: page?.nextCursor }) }) - protocol.setRequestHandler(GetPromptRequestSchema, async () => { + protocol.setRequestHandler("prompts/get", async () => { if (state.requestDelay) await Bun.sleep(state.requestDelay) return { messages: [{ role: "user", content: { type: "text", text: "prompt result" } }] } }) } if (capabilities.resources) { - protocol.setRequestHandler(ListResourcesRequestSchema, (request) => { + protocol.setRequestHandler("resources/list", (request) => { const page = state.resourcePages?.[request.params?.cursor ?? "initial"] return Promise.resolve({ resources: page?.items ?? state.resources, nextCursor: page?.nextCursor }) }) - protocol.setRequestHandler(ListResourceTemplatesRequestSchema, (request) => { + protocol.setRequestHandler("resources/templates/list", (request) => { const page = state.resourceTemplatePages?.[request.params?.cursor ?? "initial"] return Promise.resolve({ resourceTemplates: page?.items ?? state.resourceTemplates, nextCursor: page?.nextCursor, }) }) - protocol.setRequestHandler(ReadResourceRequestSchema, async (request) => { + protocol.setRequestHandler("resources/read", async (request) => { if (state.requestDelay) await Bun.sleep(state.requestDelay) return { contents: [{ uri: request.params.uri, text: "resource result" }] } }) @@ -145,7 +139,7 @@ function hangingLifecycleServer() { return Effect.acquireRelease( Effect.promise(async () => { const protocol = new Server({ name: "mcp-lifecycle-hanging", version: "1.0.0" }, { capabilities: { tools: {} } }) - protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] })) + protocol.setRequestHandler("tools/list", () => Promise.resolve({ tools: [] })) const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID(), enableJsonResponse: true, @@ -284,7 +278,7 @@ it.instance("follows cursors when listing tools, prompts, resources, and templat }), ) -it.instance("accepts empty cursors and rejects repeated cursors", () => +it.instance("accepts empty cursors and terminates on repeated cursors", () => Effect.gen(function* () { const empty = yield* lifecycleServer({ capabilities: { prompts: {} } }) empty.state.promptPages = { @@ -301,7 +295,8 @@ it.instance("accepts empty cursors and rejects repeated cursors", () => const result = yield* mcp.add("looping-cursor", remote(looping.url)) expect(Object.keys(yield* mcp.prompts())).toEqual(["empty-cursor:prompt-one", "empty-cursor:prompt-two"]) - expect(statusName(result.status, "looping-cursor")).toBe("failed") + expect(statusName(result.status, "looping-cursor")).toBe("connected") + expect(Object.keys(yield* mcp.tools())).toEqual([]) }), ) diff --git a/packages/opencode/test/mcp/oauth-auto-connect.test.ts b/packages/opencode/test/mcp/oauth-auto-connect.test.ts index 5f8889068c..735dfaf8be 100644 --- a/packages/opencode/test/mcp/oauth-auto-connect.test.ts +++ b/packages/opencode/test/mcp/oauth-auto-connect.test.ts @@ -1,7 +1,5 @@ import { expect } from "bun:test" -import { Server } from "@modelcontextprotocol/sdk/server/index.js" -import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js" -import { ListResourcesRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" +import { Server, WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { FSUtil } from "@opencode-ai/core/fs-util" @@ -40,13 +38,13 @@ function serveOAuthMcp(options: OAuthMcpOptions = {}) { let requiresAuth = true if (capabilities === "tools") { - protocol.setRequestHandler(ListToolsRequestSchema, () => { + protocol.setRequestHandler("tools/list", () => { listToolsCalls++ return Promise.resolve({ tools: [{ name: "test_tool", inputSchema: { type: "object" } }] }) }) } if (capabilities === "resources") { - protocol.setRequestHandler(ListResourcesRequestSchema, () => + protocol.setRequestHandler("resources/list", () => Promise.resolve({ resources: [{ name: "docs", uri: "docs://readme" }] }), ) } diff --git a/packages/opencode/test/mcp/oauth-browser.test.ts b/packages/opencode/test/mcp/oauth-browser.test.ts index 9573805a9a..507b59b691 100644 --- a/packages/opencode/test/mcp/oauth-browser.test.ts +++ b/packages/opencode/test/mcp/oauth-browser.test.ts @@ -1,7 +1,5 @@ import { expect } from "bun:test" -import { Server } from "@modelcontextprotocol/sdk/server/index.js" -import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js" -import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" +import { Server, WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Deferred, Effect, Layer, Option } from "effect" import { Config } from "../../src/config/config" @@ -41,7 +39,7 @@ const serveOAuthMcp = Effect.acquireRelease( Effect.promise(async () => { const requests: Array<{ pathname: string; headers: Headers }> = [] const protocol = new Server({ name: "oauth-browser", version: "1.0.0" }, { capabilities: { tools: {} } }) - protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] })) + protocol.setRequestHandler("tools/list", () => Promise.resolve({ tools: [] })) const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID(), enableJsonResponse: true, diff --git a/packages/opencode/test/mcp/oauth-callback.test.ts b/packages/opencode/test/mcp/oauth-callback.test.ts index 1666a37142..b7db18034f 100644 --- a/packages/opencode/test/mcp/oauth-callback.test.ts +++ b/packages/opencode/test/mcp/oauth-callback.test.ts @@ -74,7 +74,7 @@ describe("McpOAuthCallback.ensureRunning", () => { const response = await fetch(`${redirectUri}?code=code&state=success`) expect(response.status).toBe(200) - expect(await callback).toBe("code") + expect(await callback).toEqual({ code: "code", iss: undefined }) expect(McpOAuthCallback.isRunning()).toBe(false) }) diff --git a/packages/opencode/test/mcp/oauth-provider.test.ts b/packages/opencode/test/mcp/oauth-provider.test.ts index 249c49e8f9..64c2cb6687 100644 --- a/packages/opencode/test/mcp/oauth-provider.test.ts +++ b/packages/opencode/test/mcp/oauth-provider.test.ts @@ -1,5 +1,4 @@ import { test, expect, describe } from "bun:test" -import { determineScope } from "@modelcontextprotocol/sdk/client/auth.js" import { McpOAuthProvider, OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "../../src/mcp/oauth-provider" import type { McpAuth } from "../../src/mcp/auth" @@ -60,43 +59,3 @@ describe("McpOAuthProvider.clientMetadata", () => { expect(provider.clientMetadata.token_endpoint_auth_method).toBe("none") }) }) - -describe("MCP OAuth scope selection", () => { - test("adds offline_access when the authorization server and client support refresh tokens", () => { - expect( - determineScope({ - resourceMetadata: { - resource: "https://mcp.example.com/mcp", - scopes_supported: ["resource.read"], - }, - authServerMetadata: { - issuer: "https://auth.example.com", - authorization_endpoint: "https://auth.example.com/authorize", - token_endpoint: "https://auth.example.com/token", - response_types_supported: ["code"], - scopes_supported: ["resource.read", "offline_access"], - }, - clientMetadata: makeProvider({}).clientMetadata, - }), - ).toBe("resource.read offline_access") - }) - - test("does not add unsupported authorization server scopes", () => { - expect( - determineScope({ - resourceMetadata: { - resource: "https://mcp.example.com/mcp", - scopes_supported: ["resource.read"], - }, - authServerMetadata: { - issuer: "https://auth.example.com", - authorization_endpoint: "https://auth.example.com/authorize", - token_endpoint: "https://auth.example.com/token", - response_types_supported: ["code"], - scopes_supported: ["resource.read"], - }, - clientMetadata: makeProvider({}).clientMetadata, - }), - ).toBe("resource.read") - }) -}) diff --git a/packages/opencode/test/mcp/session-recovery.test.ts b/packages/opencode/test/mcp/session-recovery.test.ts index 6586508220..7011ec564b 100644 --- a/packages/opencode/test/mcp/session-recovery.test.ts +++ b/packages/opencode/test/mcp/session-recovery.test.ts @@ -2,7 +2,7 @@ import path from "node:path" import { describe, expect, test } from "bun:test" describe("mcp session recovery", () => { - test("reinitializes and retries once after a session-bound POST returns 404", async () => { + test.skip("reinitializes and retries once after a session-bound POST returns 404", async () => { const child = Bun.spawn([process.execPath, path.join(import.meta.dir, "../fixture/mcp-session-recovery.ts")], { cwd: path.join(import.meta.dir, "../.."), stdout: "pipe", diff --git a/packages/opencode/test/tool/code-mode-integration.test.ts b/packages/opencode/test/tool/code-mode-integration.test.ts index 671acd8962..32cb420681 100644 --- a/packages/opencode/test/tool/code-mode-integration.test.ts +++ b/packages/opencode/test/tool/code-mode-integration.test.ts @@ -8,15 +8,14 @@ import { Session } from "@/session/session" import { Tool } from "@/tool/tool" import * as Truncate from "@/tool/truncate" import { MessageID, SessionID } from "@/session/schema" -import { Server } from "@modelcontextprotocol/sdk/server/index.js" -import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js" -import type { Client } from "@modelcontextprotocol/sdk/client/index.js" +import { Server } from "@modelcontextprotocol/server" import { - CallToolRequestSchema, + InMemoryTransport, LATEST_PROTOCOL_VERSION, - ListToolsRequestSchema, + type CallToolResult, + type Client, type Tool as MCPToolDef, -} from "@modelcontextprotocol/sdk/types.js" +} from "@modelcontextprotocol/client" import { Cause, Effect, Exit, Layer } from "effect" const PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" @@ -100,7 +99,7 @@ const TOOL_DEFS: MCPToolDef[] = [ }, ] as MCPToolDef[] -function handleCall(name: string, args: Record) { +function handleCall(name: string, args: Record): CallToolResult { switch (name) { case "get_text": return { content: [{ type: "text", text: `hello ${args.name}` }] } @@ -122,8 +121,8 @@ let description: string async function buildTool() { const server = new Server({ name: SERVER, version: "1.0.0" }, { capabilities: { tools: {} } }) - server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_DEFS })) - server.setRequestHandler(CallToolRequestSchema, async (req) => + server.setRequestHandler("tools/list", async () => ({ tools: TOOL_DEFS })) + server.setRequestHandler("tools/call", async (req) => handleCall(req.params.name, (req.params.arguments ?? {}) as Record), ) diff --git a/packages/opencode/test/tool/code-mode.test.ts b/packages/opencode/test/tool/code-mode.test.ts index 34b3faa610..cc32d2a5f3 100644 --- a/packages/opencode/test/tool/code-mode.test.ts +++ b/packages/opencode/test/tool/code-mode.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { CODE_MODE_TOOL, CodeModeTool, Parameters, describeCatalog } from "@/tool/code-mode" -import type { Tool as MCPToolDef } from "@modelcontextprotocol/sdk/types.js" +import type { Tool as MCPToolDef } from "@modelcontextprotocol/client" import type { PermissionV1 } from "@opencode-ai/core/v1/permission" import { Agent } from "@/agent/agent" import { MCP } from "@/mcp" diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index c8c5fac595..c0810a6d7e 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -20,7 +20,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" import { MCP } from "@/mcp" -import type { Tool as MCPToolDef } from "@modelcontextprotocol/sdk/types.js" +import type { Tool as MCPToolDef } from "@modelcontextprotocol/client" const configLayer = TestConfig.layer({ directories: () => InstanceState.directory.pipe(Effect.map((dir) => [path.join(dir, ".opencode")])), diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 9ed0084aac..babdbc9c51 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -2328,6 +2328,7 @@ export class Auth2 extends HeyApiClient { directory?: string workspace?: string code?: string + iss?: string }, options?: Options, ) { @@ -2340,6 +2341,7 @@ export class Auth2 extends HeyApiClient { { in: "query", key: "directory" }, { in: "query", key: "workspace" }, { in: "body", key: "code" }, + { in: "body", key: "iss" }, ], }, ], diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 42d224780d..f0db3236ea 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -8563,6 +8563,7 @@ export type McpAuthStartResponse = McpAuthStartResponses[keyof McpAuthStartRespo export type McpAuthCallbackData = { body?: { code: string + iss?: string } path: { name: string diff --git a/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch b/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch deleted file mode 100644 index 13b8000a01..0000000000 --- a/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch +++ /dev/null @@ -1,629 +0,0 @@ -diff --git a/dist/cjs/client/index.d.ts b/dist/cjs/client/index.d.ts -index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644 ---- a/dist/cjs/client/index.d.ts -+++ b/dist/cjs/client/index.d.ts -@@ -428,6 +428,8 @@ export declare class Client>; -+ callTool(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise>; - callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{ - [x: string]: unknown; - content: ({ -diff --git a/dist/esm/client/index.d.ts b/dist/esm/client/index.d.ts -index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644 ---- a/dist/esm/client/index.d.ts -+++ b/dist/esm/client/index.d.ts -@@ -428,6 +428,8 @@ export declare class Client>; -+ callTool(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise>; - callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{ - [x: string]: unknown; - content: ({ -diff --git a/dist/cjs/client/index.js b/dist/cjs/client/index.js -index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c101584c84 100644 ---- a/dist/cjs/client/index.js -+++ b/dist/cjs/client/index.js -@@ -288,41 +288,16 @@ class Client extends protocol_js_1.Protocol { - } - async connect(transport, options) { - await super.connect(transport); -+ transport.onsessionexpired = async () => { -+ await this._initialize(transport); -+ }; - // When transport sessionId is already set this means we are trying to reconnect. - // In this case we don't need to initialize again. - if (transport.sessionId !== undefined) { - return; - } - try { -- const result = await this.request({ -- method: 'initialize', -- params: { -- protocolVersion: types_js_1.LATEST_PROTOCOL_VERSION, -- capabilities: this._capabilities, -- clientInfo: this._clientInfo -- } -- }, types_js_1.InitializeResultSchema, options); -- if (result === undefined) { -- throw new Error(`Server sent invalid initialize result: ${result}`); -- } -- if (!types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) { -- throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`); -- } -- this._serverCapabilities = result.capabilities; -- this._serverVersion = result.serverInfo; -- // HTTP transports must set the protocol version in each header after initialization. -- if (transport.setProtocolVersion) { -- transport.setProtocolVersion(result.protocolVersion); -- } -- this._instructions = result.instructions; -- await this.notification({ -- method: 'notifications/initialized' -- }); -- // Set up list changed handlers now that we know server capabilities -- if (this._pendingListChangedConfig) { -- this._setupListChangedHandlers(this._pendingListChangedConfig); -- this._pendingListChangedConfig = undefined; -- } -+ await this._initialize(transport, options); - } - catch (error) { - // Disconnect if initialization fails. -@@ -330,6 +305,37 @@ class Client extends protocol_js_1.Protocol { - throw error; - } - } -+ async _initialize(transport, options) { -+ const result = await this.request({ -+ method: 'initialize', -+ params: { -+ protocolVersion: types_js_1.LATEST_PROTOCOL_VERSION, -+ capabilities: this._capabilities, -+ clientInfo: this._clientInfo -+ } -+ }, types_js_1.InitializeResultSchema, options); -+ if (result === undefined) { -+ throw new Error(`Server sent invalid initialize result: ${result}`); -+ } -+ if (!types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) { -+ throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`); -+ } -+ this._serverCapabilities = result.capabilities; -+ this._serverVersion = result.serverInfo; -+ // HTTP transports must set the protocol version in each header after initialization. -+ if (transport.setProtocolVersion) { -+ transport.setProtocolVersion(result.protocolVersion); -+ } -+ this._instructions = result.instructions; -+ await this.notification({ -+ method: 'notifications/initialized' -+ }); -+ // Set up list changed handlers now that we know server capabilities -+ if (this._pendingListChangedConfig) { -+ this._setupListChangedHandlers(this._pendingListChangedConfig); -+ this._pendingListChangedConfig = undefined; -+ } -+ } - /** - * After initialization has completed, this will be populated with the server's reported capabilities. - */ -@@ -541,9 +547,11 @@ class Client extends protocol_js_1.Protocol { - * Called after listTools() to pre-compile validators for better performance. - */ -- cacheToolMetadata(tools) { -- this._cachedToolOutputValidators.clear(); -- this._cachedKnownTaskTools.clear(); -- this._cachedRequiredTaskTools.clear(); -+ cacheToolMetadata(tools, reset = true) { -+ if (reset) { -+ this._cachedToolOutputValidators.clear(); -+ this._cachedKnownTaskTools.clear(); -+ this._cachedRequiredTaskTools.clear(); -+ } - for (const tool of tools) { - // If the tool has an outputSchema, create and cache the validator - if (tool.outputSchema) { -@@ -569,7 +577,7 @@ class Client extends protocol_js_1.Protocol { - async listTools(params, options) { - const result = await this.request({ method: 'tools/list', params }, types_js_1.ListToolsResultSchema, options); - // Cache the tools and their output schemas for future validation -- this.cacheToolMetadata(result.tools); -+ this.cacheToolMetadata(result.tools, params?.cursor === undefined); - return result; - } - /** -diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp.js -index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eedff5443c9 100644 ---- a/dist/cjs/client/streamableHttp.js -+++ b/dist/cjs/client/streamableHttp.js -@@ -290,7 +290,38 @@ class StreamableHTTPClientTransport { - this.onclose?.(); - } - async send(message, options) { -+ return this._send(message, options, false); -+ } -+ async _recoverSession(expiredSessionId) { -+ if (this._sessionRecovery) { -+ await this._sessionRecovery; -+ return true; -+ } -+ if (this._sessionId !== expiredSessionId) -+ return true; -+ this._sessionId = undefined; -+ this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired?.()); - try { -+ await this._sessionRecovery; -+ } -+ catch (error) { -+ this._sessionId = undefined; -+ await this.close(); -+ throw error; -+ } -+ finally { -+ this._sessionRecovery = undefined; -+ } -+ return true; -+ } -+ async _send(message, options, isSessionRetry) { -+ try { -+ if (this._sessionRecovery && !(0, types_js_1.isInitializeRequest)(message) && !(0, types_js_1.isInitializedNotification)(message)) { -+ await this._sessionRecovery; -+ if (options?.isRequestActive?.() === false) { -+ throw new Error('Request is no longer active'); -+ } -+ } - const { resumptionToken, onresumptiontoken } = options || {}; - if (resumptionToken) { - // If we have at last event ID, we need to reconnect the SSE stream -@@ -298,6 +329,7 @@ class StreamableHTTPClientTransport { - return; - } - const headers = await this._commonHeaders(); -+ const requestSessionId = headers.get('mcp-session-id') ?? undefined; - headers.set('content-type', 'application/json'); - headers.set('accept', 'application/json, text/event-stream'); - const init = { -@@ -310,11 +342,20 @@ class StreamableHTTPClientTransport { - const response = await (this._fetch ?? fetch)(this._url, init); - // Handle session ID received during initialization - const sessionId = response.headers.get('mcp-session-id'); -- if (sessionId) { -+ if (sessionId && (requestSessionId === undefined || this._sessionId === requestSessionId)) { - this._sessionId = sessionId; - } - if (!response.ok) { - const text = await response.text().catch(() => null); -+ if (response.status === 404 && requestSessionId && !isSessionRetry && !(0, types_js_1.isInitializedNotification)(message)) { -+ const recovered = await this._recoverSession(requestSessionId); -+ if (options?.isRequestActive?.() === false) { -+ throw new Error('Request is no longer active'); -+ } -+ if (recovered) { -+ return this._send(message, options, true); -+ } -+ } - if (response.status === 401 && this._authProvider) { - // Prevent infinite recursion when server returns 401 after successful auth - if (this._hasCompletedAuthFlow) { -@@ -335,7 +376,7 @@ class StreamableHTTPClientTransport { - // Mark that we completed auth flow - this._hasCompletedAuthFlow = true; - // Purposely _not_ awaited, so we don't call onerror twice -- return this.send(message); -+ return this._send(message, options, isSessionRetry); - } - if (response.status === 403 && this._authProvider) { - const { resourceMetadataUrl, scope, error } = (0, auth_js_1.extractWWWAuthenticateParams)(response); -@@ -362,7 +403,7 @@ class StreamableHTTPClientTransport { - if (result !== 'AUTHORIZED') { - throw new auth_js_1.UnauthorizedError(); - } -- return this.send(message); -+ return this._send(message, options, isSessionRetry); - } - } - throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`); -diff --git a/dist/cjs/shared/protocol.js b/dist/cjs/shared/protocol.js -index 3617e787f0ba70447c99501aee7aa67584d89758..4a96d6a0328fa348b96f3869ab7e0bb77538182b 100644 ---- a/dist/cjs/shared/protocol.js -+++ b/dist/cjs/shared/protocol.js -@@ -744,7 +744,12 @@ class Protocol { - } - else { - // No related task - send through transport normally -- this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => { -+ this._transport.send(jsonrpcRequest, { -+ relatedRequestId, -+ resumptionToken, -+ onresumptiontoken, -+ isRequestActive: () => this._responseHandlers.has(messageId) -+ }).catch(error => { - this._cleanupTimeout(messageId); - reject(error); - }); -diff --git a/dist/cjs/client/auth.d.ts b/dist/cjs/client/auth.d.ts -index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644 ---- a/dist/cjs/client/auth.d.ts -+++ b/dist/cjs/client/auth.d.ts -@@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise; -+/** -+ * Selects scopes per the MCP spec and augments them for refresh token support. -+ */ -+export declare function determineScope(options: { -+ requestedScope?: string; -+ resourceMetadata?: OAuthProtectedResourceMetadata; -+ authServerMetadata?: AuthorizationServerMetadata; -+ clientMetadata: OAuthClientMetadata; -+}): string | undefined; - /** - * Orchestrates the full auth flow with a server. - * -diff --git a/dist/cjs/client/auth.js b/dist/cjs/client/auth.js -index c2e4fa91d26f5336889f6afa416147db75fc4872..178d7cfd96412d53bc14bbc13a8f76c11f727ee7 100644 ---- a/dist/cjs/client/auth.js -+++ b/dist/cjs/client/auth.js -@@ -7,6 +7,7 @@ exports.UnauthorizedError = void 0; - exports.selectClientAuthMethod = selectClientAuthMethod; - exports.parseErrorResponse = parseErrorResponse; - exports.auth = auth; -+exports.determineScope = determineScope; - exports.isHttpsUrl = isHttpsUrl; - exports.selectResourceURL = selectResourceURL; - exports.extractWWWAuthenticateParams = extractWWWAuthenticateParams; -@@ -186,6 +187,19 @@ async function auth(provider, options) { - throw error; - } - } -+/** -+ * Selects scopes per the MCP spec and augments them for refresh token support. -+ */ -+function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) { -+ let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope; -+ if (effectiveScope && -+ authServerMetadata?.scopes_supported?.includes('offline_access') && -+ !effectiveScope.split(' ').includes('offline_access') && -+ clientMetadata.grant_types?.includes('refresh_token')) { -+ effectiveScope = `${effectiveScope} offline_access`; -+ } -+ return effectiveScope; -+} - async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) { - // Check if the provider has cached discovery state to skip discovery - const cachedState = await provider.discoveryState?.(); -@@ -241,12 +255,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res - }); - } - const resource = await selectResourceURL(serverUrl, provider, resourceMetadata); -- // Apply scope selection strategy (SEP-835): -- // 1. WWW-Authenticate scope (passed via `scope` param) -- // 2. PRM scopes_supported -- // 3. Client metadata scope (user-configured fallback) -- // The resolved scope is used consistently for both DCR and the authorization request. -- const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope; -+ const resolvedScope = determineScope({ -+ requestedScope: scope, -+ resourceMetadata, -+ authServerMetadata: metadata, -+ clientMetadata: provider.clientMetadata -+ }); - // Handle client registration if needed - let clientInformation = await Promise.resolve(provider.clientInformation()); - if (!clientInformation) { -@@ -741,7 +755,7 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo - if (scope) { - authorizationUrl.searchParams.set('scope', scope); - } -- if (scope?.includes('offline_access')) { -+ if (scope?.split(' ').includes('offline_access')) { - // if the request includes the OIDC-only "offline_access" scope, - // we need to set the prompt to "consent" to ensure the user is prompted to grant offline access - // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess -diff --git a/dist/esm/client/auth.d.ts b/dist/esm/client/auth.d.ts -index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644 ---- a/dist/esm/client/auth.d.ts -+++ b/dist/esm/client/auth.d.ts -@@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise; -+/** -+ * Selects scopes per the MCP spec and augments them for refresh token support. -+ */ -+export declare function determineScope(options: { -+ requestedScope?: string; -+ resourceMetadata?: OAuthProtectedResourceMetadata; -+ authServerMetadata?: AuthorizationServerMetadata; -+ clientMetadata: OAuthClientMetadata; -+}): string | undefined; - /** - * Orchestrates the full auth flow with a server. - * -diff --git a/dist/esm/client/auth.js b/dist/esm/client/auth.js -index e183040fc2bba22ca1ccc784984f3310854403b7..d367661e580ee61a96654f7af78b2af61dcad98b 100644 ---- a/dist/esm/client/auth.js -+++ b/dist/esm/client/auth.js -@@ -161,6 +161,19 @@ export async function auth(provider, options) { - throw error; - } - } -+/** -+ * Selects scopes per the MCP spec and augments them for refresh token support. -+ */ -+export function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) { -+ let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope; -+ if (effectiveScope && -+ authServerMetadata?.scopes_supported?.includes('offline_access') && -+ !effectiveScope.split(' ').includes('offline_access') && -+ clientMetadata.grant_types?.includes('refresh_token')) { -+ effectiveScope = `${effectiveScope} offline_access`; -+ } -+ return effectiveScope; -+} - async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) { - // Check if the provider has cached discovery state to skip discovery - const cachedState = await provider.discoveryState?.(); -@@ -216,12 +229,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res - }); - } - const resource = await selectResourceURL(serverUrl, provider, resourceMetadata); -- // Apply scope selection strategy (SEP-835): -- // 1. WWW-Authenticate scope (passed via `scope` param) -- // 2. PRM scopes_supported -- // 3. Client metadata scope (user-configured fallback) -- // The resolved scope is used consistently for both DCR and the authorization request. -- const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope; -+ const resolvedScope = determineScope({ -+ requestedScope: scope, -+ resourceMetadata, -+ authServerMetadata: metadata, -+ clientMetadata: provider.clientMetadata -+ }); - // Handle client registration if needed - let clientInformation = await Promise.resolve(provider.clientInformation()); - if (!clientInformation) { -@@ -716,7 +729,7 @@ export async function startAuthorization(authorizationServerUrl, { metadata, cli - if (scope) { - authorizationUrl.searchParams.set('scope', scope); - } -- if (scope?.includes('offline_access')) { -+ if (scope?.split(' ').includes('offline_access')) { - // if the request includes the OIDC-only "offline_access" scope, - // we need to set the prompt to "consent" to ensure the user is prompted to grant offline access - // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess -diff --git a/dist/esm/client/index.js b/dist/esm/client/index.js -index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8eb9e4caff 100644 ---- a/dist/esm/client/index.js -+++ b/dist/esm/client/index.js -@@ -284,41 +284,16 @@ export class Client extends Protocol { - } - async connect(transport, options) { - await super.connect(transport); -+ transport.onsessionexpired = async () => { -+ await this._initialize(transport); -+ }; - // When transport sessionId is already set this means we are trying to reconnect. - // In this case we don't need to initialize again. - if (transport.sessionId !== undefined) { - return; - } - try { -- const result = await this.request({ -- method: 'initialize', -- params: { -- protocolVersion: LATEST_PROTOCOL_VERSION, -- capabilities: this._capabilities, -- clientInfo: this._clientInfo -- } -- }, InitializeResultSchema, options); -- if (result === undefined) { -- throw new Error(`Server sent invalid initialize result: ${result}`); -- } -- if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) { -- throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`); -- } -- this._serverCapabilities = result.capabilities; -- this._serverVersion = result.serverInfo; -- // HTTP transports must set the protocol version in each header after initialization. -- if (transport.setProtocolVersion) { -- transport.setProtocolVersion(result.protocolVersion); -- } -- this._instructions = result.instructions; -- await this.notification({ -- method: 'notifications/initialized' -- }); -- // Set up list changed handlers now that we know server capabilities -- if (this._pendingListChangedConfig) { -- this._setupListChangedHandlers(this._pendingListChangedConfig); -- this._pendingListChangedConfig = undefined; -- } -+ await this._initialize(transport, options); - } - catch (error) { - // Disconnect if initialization fails. -@@ -326,6 +301,37 @@ export class Client extends Protocol { - throw error; - } - } -+ async _initialize(transport, options) { -+ const result = await this.request({ -+ method: 'initialize', -+ params: { -+ protocolVersion: LATEST_PROTOCOL_VERSION, -+ capabilities: this._capabilities, -+ clientInfo: this._clientInfo -+ } -+ }, InitializeResultSchema, options); -+ if (result === undefined) { -+ throw new Error(`Server sent invalid initialize result: ${result}`); -+ } -+ if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) { -+ throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`); -+ } -+ this._serverCapabilities = result.capabilities; -+ this._serverVersion = result.serverInfo; -+ // HTTP transports must set the protocol version in each header after initialization. -+ if (transport.setProtocolVersion) { -+ transport.setProtocolVersion(result.protocolVersion); -+ } -+ this._instructions = result.instructions; -+ await this.notification({ -+ method: 'notifications/initialized' -+ }); -+ // Set up list changed handlers now that we know server capabilities -+ if (this._pendingListChangedConfig) { -+ this._setupListChangedHandlers(this._pendingListChangedConfig); -+ this._pendingListChangedConfig = undefined; -+ } -+ } - /** - * After initialization has completed, this will be populated with the server's reported capabilities. - */ -@@ -537,9 +543,11 @@ export class Client extends Protocol { - * Called after listTools() to pre-compile validators for better performance. - */ -- cacheToolMetadata(tools) { -- this._cachedToolOutputValidators.clear(); -- this._cachedKnownTaskTools.clear(); -- this._cachedRequiredTaskTools.clear(); -+ cacheToolMetadata(tools, reset = true) { -+ if (reset) { -+ this._cachedToolOutputValidators.clear(); -+ this._cachedKnownTaskTools.clear(); -+ this._cachedRequiredTaskTools.clear(); -+ } - for (const tool of tools) { - // If the tool has an outputSchema, create and cache the validator - if (tool.outputSchema) { -@@ -565,7 +573,7 @@ export class Client extends Protocol { - async listTools(params, options) { - const result = await this.request({ method: 'tools/list', params }, ListToolsResultSchema, options); - // Cache the tools and their output schemas for future validation -- this.cacheToolMetadata(result.tools); -+ this.cacheToolMetadata(result.tools, params?.cursor === undefined); - return result; - } - /** -diff --git a/dist/esm/client/streamableHttp.js b/dist/esm/client/streamableHttp.js -index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da884fcc627a 100644 ---- a/dist/esm/client/streamableHttp.js -+++ b/dist/esm/client/streamableHttp.js -@@ -1,5 +1,5 @@ - import { createFetchWithInit, normalizeHeaders } from '../shared/transport.js'; --import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js'; -+import { isInitializedNotification, isInitializeRequest, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js'; - import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js'; - import { EventSourceParserStream } from 'eventsource-parser/stream'; - // Default reconnection options for StreamableHTTP connections -@@ -286,7 +286,38 @@ export class StreamableHTTPClientTransport { - this.onclose?.(); - } - async send(message, options) { -+ return this._send(message, options, false); -+ } -+ async _recoverSession(expiredSessionId) { -+ if (this._sessionRecovery) { -+ await this._sessionRecovery; -+ return true; -+ } -+ if (this._sessionId !== expiredSessionId) -+ return true; -+ this._sessionId = undefined; -+ this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired?.()); - try { -+ await this._sessionRecovery; -+ } -+ catch (error) { -+ this._sessionId = undefined; -+ await this.close(); -+ throw error; -+ } -+ finally { -+ this._sessionRecovery = undefined; -+ } -+ return true; -+ } -+ async _send(message, options, isSessionRetry) { -+ try { -+ if (this._sessionRecovery && !isInitializeRequest(message) && !isInitializedNotification(message)) { -+ await this._sessionRecovery; -+ if (options?.isRequestActive?.() === false) { -+ throw new Error('Request is no longer active'); -+ } -+ } - const { resumptionToken, onresumptiontoken } = options || {}; - if (resumptionToken) { - // If we have at last event ID, we need to reconnect the SSE stream -@@ -294,6 +325,7 @@ export class StreamableHTTPClientTransport { - return; - } - const headers = await this._commonHeaders(); -+ const requestSessionId = headers.get('mcp-session-id') ?? undefined; - headers.set('content-type', 'application/json'); - headers.set('accept', 'application/json, text/event-stream'); - const init = { -@@ -306,11 +338,20 @@ export class StreamableHTTPClientTransport { - const response = await (this._fetch ?? fetch)(this._url, init); - // Handle session ID received during initialization - const sessionId = response.headers.get('mcp-session-id'); -- if (sessionId) { -+ if (sessionId && (requestSessionId === undefined || this._sessionId === requestSessionId)) { - this._sessionId = sessionId; - } - if (!response.ok) { - const text = await response.text().catch(() => null); -+ if (response.status === 404 && requestSessionId && !isSessionRetry && !isInitializedNotification(message)) { -+ const recovered = await this._recoverSession(requestSessionId); -+ if (options?.isRequestActive?.() === false) { -+ throw new Error('Request is no longer active'); -+ } -+ if (recovered) { -+ return this._send(message, options, true); -+ } -+ } - if (response.status === 401 && this._authProvider) { - // Prevent infinite recursion when server returns 401 after successful auth - if (this._hasCompletedAuthFlow) { -@@ -331,7 +372,7 @@ export class StreamableHTTPClientTransport { - // Mark that we completed auth flow - this._hasCompletedAuthFlow = true; - // Purposely _not_ awaited, so we don't call onerror twice -- return this.send(message); -+ return this._send(message, options, isSessionRetry); - } - if (response.status === 403 && this._authProvider) { - const { resourceMetadataUrl, scope, error } = extractWWWAuthenticateParams(response); -@@ -358,7 +399,7 @@ export class StreamableHTTPClientTransport { - if (result !== 'AUTHORIZED') { - throw new UnauthorizedError(); - } -- return this.send(message); -+ return this._send(message, options, isSessionRetry); - } - } - throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`); -diff --git a/dist/esm/shared/protocol.js b/dist/esm/shared/protocol.js -index bfa2b7120a0f50c569364ea5264e6f811076f44f..abd8dfd707c155f71dae7aeeeeaf7547368ac749 100644 ---- a/dist/esm/shared/protocol.js -+++ b/dist/esm/shared/protocol.js -@@ -740,7 +740,12 @@ export class Protocol { - } - else { - // No related task - send through transport normally -- this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => { -+ this._transport.send(jsonrpcRequest, { -+ relatedRequestId, -+ resumptionToken, -+ onresumptiontoken, -+ isRequestActive: () => this._responseHandlers.has(messageId) -+ }).catch(error => { - this._cleanupTimeout(messageId); - reject(error); - }); From d50b9e8f9a562bb23196f5b91b2263ac0450d0e0 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 28 Jul 2026 04:20:55 +0000 Subject: [PATCH 047/129] chore: generate --- packages/sdk/openapi.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index b300754bc8..6150b75e64 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -3360,6 +3360,9 @@ "properties": { "code": { "type": "string" + }, + "iss": { + "type": "string" } }, "required": ["code"], From 5e3905118339a2970e209e7941c4d572640b9fa0 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 23:21:00 -0500 Subject: [PATCH 048/129] fix(provider): omit deprecated Gemini sampling defaults (#38924) Co-authored-by: Aiden Cline --- packages/opencode/src/provider/transform.ts | 23 +++++-- .../opencode/test/provider/transform.test.ts | 64 ++++++++++++++++++- 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 705af3d5c1..303ff11fdf 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -516,12 +516,20 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re return msgs } +const GEMINI_MODELS_WITH_SAMPLING_DEFAULTS = [ + /gemini-2[.-]5(?:[.-]|$)/, + /gemini-3-(?:flash|pro)(?:[.-]|$)/, + /gemini-3[.-]1(?:[.-]|$)/, + /gemini-3[.-]5-flash(?!-lite)(?:[.-]|$)/, +] + export function temperature(model: Provider.Model) { - const id = model.id.toLowerCase() + const id = model.api.id.toLowerCase() if (id.includes("north-mini-code")) return 1.0 if (id.includes("qwen")) return 0.55 if (id.includes("claude")) return undefined - if (id.includes("gemini")) return 1.0 + if (id.includes("gemini")) + return GEMINI_MODELS_WITH_SAMPLING_DEFAULTS.some((model) => model.test(id)) ? 1.0 : undefined if (id.includes("glm-4.6")) return 1.0 if (id.includes("glm-4.7")) return 1.0 if (id.includes("minimax-m2")) return 1.0 @@ -536,21 +544,24 @@ export function temperature(model: Provider.Model) { } export function topP(model: Provider.Model) { - const id = model.id.toLowerCase() + const id = model.api.id.toLowerCase() if (id.includes("qwen")) return 1 - if (["minimax-m2", "gemini", "kimi-k2.5", "kimi-k2p5", "kimi-k2-5"].some((s) => id.includes(s))) { + if (id.includes("gemini")) + return GEMINI_MODELS_WITH_SAMPLING_DEFAULTS.some((model) => model.test(id)) ? 0.95 : undefined + if (["minimax-m2", "kimi-k2.5", "kimi-k2p5", "kimi-k2-5"].some((s) => id.includes(s))) { return 0.95 } return undefined } export function topK(model: Provider.Model) { - const id = model.id.toLowerCase() + const id = model.api.id.toLowerCase() if (id.includes("minimax-m2")) { if (["m2.", "m25", "m21"].some((s) => id.includes(s))) return 40 return 20 } - if (id.includes("gemini")) return 64 + if (id.includes("gemini")) + return GEMINI_MODELS_WITH_SAMPLING_DEFAULTS.some((model) => model.test(id)) ? 64 : undefined return undefined } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 93e166c4a8..0b88fa59df 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3186,7 +3186,69 @@ describe("ProviderTransform.message - cache control on gateway", () => { describe("ProviderTransform.temperature - Cohere North", () => { test("defaults north-mini-code models to 1.0", () => { - expect(ProviderTransform.temperature({ id: "cohere/North-Mini-Code-1-0-latest" } as any)).toBe(1.0) + expect( + ProviderTransform.temperature({ + id: "cohere/North-Mini-Code-1-0-latest", + api: { id: "North-Mini-Code-1-0-latest" }, + } as any), + ).toBe(1.0) + }) +}) + +describe("ProviderTransform sampling defaults - Gemini", () => { + const model = (id: string) => + ({ + id: `google/${id}`, + api: { id }, + }) as any + + const alias = (id: string, apiID: string) => + ({ + id, + api: { id: apiID }, + }) as any + + test.each([ + "gemini-3.5-flash-lite", + "gemini-3-5-flash-lite", + "gemini-3.6-flash", + "gemini-3-6-flash", + "gemini-4-pro", + "gemini-future", + ])("omits deprecated sampling controls for %s", (id) => { + expect(ProviderTransform.temperature(model(id))).toBeUndefined() + expect(ProviderTransform.topP(model(id))).toBeUndefined() + expect(ProviderTransform.topK(model(id))).toBeUndefined() + }) + + test.each([ + "gemini-2.5-flash", + "gemini-2.5-pro", + "gemini-2.5-flash-lite", + "gemini-2-5-flash-lite", + "gemini-3-flash-preview", + "gemini-3-pro-image", + "gemini-3.1-flash-lite", + "gemini-3.1-pro-preview", + "gemini-3-1-pro-preview", + "gemini-3.5-flash", + "gemini-3-5-flash", + ])("preserves sampling defaults for %s", (id) => { + expect(ProviderTransform.temperature(model(id))).toBe(1) + expect(ProviderTransform.topP(model(id))).toBe(0.95) + expect(ProviderTransform.topK(model(id))).toBe(64) + }) + + test("uses the API model ID for configured aliases", () => { + const deprecated = alias("google/gemini-3.5-flash", "google/gemini-3.6-flash") + expect(ProviderTransform.temperature(deprecated)).toBeUndefined() + expect(ProviderTransform.topP(deprecated)).toBeUndefined() + expect(ProviderTransform.topK(deprecated)).toBeUndefined() + + const supported = alias("my-gemini", "google/gemini-2.5-flash") + expect(ProviderTransform.temperature(supported)).toBe(1) + expect(ProviderTransform.topP(supported)).toBe(0.95) + expect(ProviderTransform.topK(supported)).toBe(64) }) }) From 172d08cb981248023c82f5b9d138763b27d69783 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 28 Jul 2026 04:34:03 +0000 Subject: [PATCH 049/129] 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 06650c6488..ce982c7ac4 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-szhy0258K6IMWi3WRrAzRfhEEIDkGTS04KQwQw+DXyI=", - "aarch64-linux": "sha256-eVqUhXGKgx7rmRzpdr9G59Xrxvj2J0Dhx4Q7QRBHKiQ=", - "aarch64-darwin": "sha256-r/xAkD/yktfxOVTAArWYb0ZMWe/hEqh80dkYsigRkm8=", - "x86_64-darwin": "sha256-xEKSQZM4gYoz9RoaHEvuxrT8AFXHx8hMD1oKGi8lc3Y=" + "x86_64-linux": "sha256-YH/hjSpK7t5RKE2ie6l+EDKaXKoqVXR11ihGXK957DQ=", + "aarch64-linux": "sha256-fJztjpptVyHStsCWxaaAaQnfxu4Uh2fv79qCag1pokg=", + "aarch64-darwin": "sha256-C9Mno6ts6f19iwPAiO9299CMpK7/dmSL5JS9JSTAmKw=", + "x86_64-darwin": "sha256-zcfjXO73bPK1JtcxekDNyGPwIkj3LMITzAB0hq2g/Ro=" } } From c3be6c496528ca6772736b46d1f539f3ca1cefab Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 28 Jul 2026 00:00:38 -0500 Subject: [PATCH 050/129] fix(mcp): honor callback port in debug (#39259) --- packages/opencode/src/cli/cmd/mcp.ts | 1 + packages/opencode/src/mcp/catalog.ts | 63 +++++++++++++--------- packages/opencode/src/mcp/index.ts | 7 +-- packages/opencode/src/session/tools.ts | 2 +- packages/opencode/src/tool/code-mode.ts | 22 +------- packages/opencode/test/mcp/catalog.test.ts | 44 ++++++++++++++- 6 files changed, 84 insertions(+), 55 deletions(-) diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index 009b6ba755..c9f74e2bc0 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -737,6 +737,7 @@ export const McpDebugCommand = effectCmd({ clientId: oauthConfig?.clientId, clientSecret: oauthConfig?.clientSecret, scope: oauthConfig?.scope, + callbackPort: oauthConfig?.callbackPort, redirectUri: oauthConfig?.redirectUri, }, { diff --git a/packages/opencode/src/mcp/catalog.ts b/packages/opencode/src/mcp/catalog.ts index 113ddbd5b8..226cd6bb25 100644 --- a/packages/opencode/src/mcp/catalog.ts +++ b/packages/opencode/src/mcp/catalog.ts @@ -1,44 +1,57 @@ -import { Client, type Tool as MCPToolDef } from "@modelcontextprotocol/client" +import { Client, type CallToolResult, type Tool as MCPToolDef } from "@modelcontextprotocol/client" import { dynamicTool, jsonSchema, type JSONSchema7, type Tool } from "ai" import { Effect } from "effect" const DEFAULT_TIMEOUT = 30_000 + +export interface McpTool { + readonly def: MCPToolDef + readonly client: Client + readonly timeout?: number +} + +export async function callTool( + tool: McpTool, + args: Record, + signal?: AbortSignal, +): Promise { + const result = await tool.client.callTool( + { name: tool.def.name, arguments: args }, + { + resetTimeoutOnProgress: true, + signal, + timeout: tool.timeout, + // The MCP SDK only sends a progress token when this hook is present, enabling timeout resets. + onprogress: () => {}, + }, + ) + if (result.isError) + throw new Error( + result.content + .flatMap((item) => (item.type === "text" ? [item.text] : [])) + .filter((text) => text.trim()) + .join("\n\n") || "MCP tool returned an error", + ) + return result +} + export function defs(client: Client, timeout?: number) { return listTools(client, timeout ?? DEFAULT_TIMEOUT).pipe(Effect.catch(() => Effect.void)) } -export function convertTool(mcpTool: MCPToolDef, client: Client, timeout?: number): Tool { +export function convertTool(tool: McpTool): Tool { const inputSchema: JSONSchema7 = { - ...(mcpTool.inputSchema as JSONSchema7), + ...(tool.def.inputSchema as JSONSchema7), type: "object", - properties: (mcpTool.inputSchema.properties ?? {}) as JSONSchema7["properties"], + properties: (tool.def.inputSchema.properties ?? {}) as JSONSchema7["properties"], additionalProperties: false, } return dynamicTool({ - description: mcpTool.description ?? "", + description: tool.def.description ?? "", inputSchema: jsonSchema(inputSchema), execute: async (args: unknown, options) => { - const result = await client.callTool( - { - name: mcpTool.name, - arguments: (args || {}) as Record, - }, - { - resetTimeoutOnProgress: true, - signal: options.abortSignal, - timeout, - // The MCP SDK only sends a progress token when this hook is present, enabling timeout resets. - onprogress: () => {}, - }, - ) - if (result.isError) - throw new Error( - result.content - .flatMap((item) => (item.type === "text" ? [item.text] : [])) - .filter((text) => text.trim()) - .join("\n\n") || "MCP tool returned an error", - ) + const result = await callTool(tool, (args || {}) as Record, options.abortSignal) if (result.content.length > 0 || result.structuredContent === undefined || result.structuredContent === null) return result return { diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 75d685b82a..939c442047 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -162,12 +162,7 @@ export interface ServerInstructions { } /** An MCP tool in its native shape; consumers adapt it to their own tool format. */ -export interface McpTool { - /** Shared cached definition; consumers must copy rather than mutate it. */ - readonly def: MCPToolDef - readonly client: MCPClient - readonly timeout?: number -} +export type McpTool = McpCatalog.McpTool export interface Interface { readonly status: () => Effect.Effect> diff --git a/packages/opencode/src/session/tools.ts b/packages/opencode/src/session/tools.ts index 0f401c7562..d93fb66f2d 100644 --- a/packages/opencode/src/session/tools.ts +++ b/packages/opencode/src/session/tools.ts @@ -388,7 +388,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { if (flags.experimentalCodeMode) return tools for (const [key, entry] of Object.entries(yield* mcp.tools())) { - const item = McpCatalog.convertTool(entry.def, entry.client, entry.timeout) + const item = McpCatalog.convertTool(entry) const execute = item.execute if (!execute) continue diff --git a/packages/opencode/src/tool/code-mode.ts b/packages/opencode/src/tool/code-mode.ts index 5d9b809a30..a046b4093d 100644 --- a/packages/opencode/src/tool/code-mode.ts +++ b/packages/opencode/src/tool/code-mode.ts @@ -145,27 +145,7 @@ const invokeChildTool = Effect.fn("CodeMode.invokeChildTool")(function* (input: ) const result: CallToolResult = yield* Effect.gen(function* () { yield* input.ctx.ask({ permission: input.entry.key, metadata: {}, patterns: ["*"], always: ["*"] }) - // Deliberately mirrors McpCatalog.convertTool's transport call so the MCP service stays free of tool-loop concerns. - return yield* Effect.promise(async () => { - const raw = await input.entry.tool.client.callTool( - { name: input.entry.tool.def.name, arguments: input.args }, - { - resetTimeoutOnProgress: true, - signal: input.ctx.abort, - timeout: input.entry.tool.timeout, - // The MCP SDK only sends a progress token when this hook is present, enabling timeout resets. - onprogress: () => {}, - }, - ) - if (raw.isError) - throw new Error( - raw.content - .flatMap((item) => (item.type === "text" ? [item.text] : [])) - .filter((text) => text.trim()) - .join("\n\n") || "MCP tool returned an error", - ) - return raw - }) + return yield* Effect.promise(() => McpCatalog.callTool(input.entry.tool, input.args, input.ctx.abort)) }).pipe( Effect.withSpan("Tool.execute", { attributes: { diff --git a/packages/opencode/test/mcp/catalog.test.ts b/packages/opencode/test/mcp/catalog.test.ts index d1367df304..3d77cb22b7 100644 --- a/packages/opencode/test/mcp/catalog.test.ts +++ b/packages/opencode/test/mcp/catalog.test.ts @@ -28,7 +28,10 @@ describe("McpCatalog.convertTool", () => { test("preserves content when structuredContent is also present", async () => { const content = [{ type: "image" as const, mimeType: "image/png", data: "AAAA" }] const structuredContent = { image: { mimeType: "image/png", data: "AAAA" } } - const converted = McpCatalog.convertTool(mcpTool(), clientReturning({ content, structuredContent })) + const converted = McpCatalog.convertTool({ + def: mcpTool(), + client: clientReturning({ content, structuredContent }), + }) const output = await converted.execute?.({}, options) @@ -37,7 +40,10 @@ describe("McpCatalog.convertTool", () => { test("falls back to structuredContent only when content is absent", async () => { const structuredContent = { results: [{ title: "one" }] } - const converted = McpCatalog.convertTool(mcpTool(), clientReturning({ content: [], structuredContent })) + const converted = McpCatalog.convertTool({ + def: mcpTool(), + client: clientReturning({ content: [], structuredContent }), + }) const output = await converted.execute?.({}, options) @@ -48,6 +54,40 @@ describe("McpCatalog.convertTool", () => { }) }) +describe("McpCatalog.callTool", () => { + test("forwards the request options", async () => { + const controller = new AbortController() + let request: unknown + let options: unknown + const client = { + callTool: async (input: unknown, config: unknown) => { + request = input + options = config + return { content: [] } + }, + } as unknown as Client + + await McpCatalog.callTool({ def: mcpTool(), client, timeout: 123 }, { value: true }, controller.signal) + + expect(request).toEqual({ name: "screenshot", arguments: { value: true } }) + expect(options).toMatchObject({ resetTimeoutOnProgress: true, signal: controller.signal, timeout: 123 }) + expect(typeof (options as { onprogress?: unknown }).onprogress).toBe("function") + }) + + test("throws text returned by an MCP tool error", async () => { + const client = clientReturning({ + isError: true, + content: [ + { type: "image", data: "AAAA", mimeType: "image/png" }, + { type: "text", text: "first" }, + { type: "text", text: "second" }, + ], + }) + + await expect(McpCatalog.callTool({ def: mcpTool(), client }, {})).rejects.toThrow("first\n\nsecond") + }) +}) + test("preserves output schema validation across paginated tool discovery", async () => { const server = new Server({ name: "pagination", version: "1.0.0" }, { capabilities: { tools: {} } }) server.setRequestHandler("tools/list", ({ params }) => From 484f00ebf44fbb9ec938b2155dad42c34fc5a7a7 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 28 Jul 2026 00:35:19 -0500 Subject: [PATCH 051/129] fix(mcp): recover expired SDK sessions (#39265) --- bun.lock | 3 +- package.json | 3 +- .../test/fixture/mcp-session-recovery.ts | 10 +- .../test/mcp/session-recovery.test.ts | 22 +- ...ontextprotocol%2Fclient@2.0.0-beta.5.patch | 214 ++++++++++++++++++ 5 files changed, 247 insertions(+), 5 deletions(-) create mode 100644 patches/@modelcontextprotocol%2Fclient@2.0.0-beta.5.patch diff --git a/bun.lock b/bun.lock index f5df0e1fad..de6eb95f9c 100644 --- a/bun.lock +++ b/bun.lock @@ -1071,8 +1071,9 @@ ], "patchedDependencies": { "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", - "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", + "@modelcontextprotocol/client@2.0.0-beta.5": "patches/@modelcontextprotocol%2Fclient@2.0.0-beta.5.patch", "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", + "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", diff --git a/package.json b/package.json index 176c7a2459..662f9a4c3c 100644 --- a/package.json +++ b/package.json @@ -155,6 +155,7 @@ "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", - "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch" + "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", + "@modelcontextprotocol/client@2.0.0-beta.5": "patches/@modelcontextprotocol%2Fclient@2.0.0-beta.5.patch" } } diff --git a/packages/opencode/test/fixture/mcp-session-recovery.ts b/packages/opencode/test/fixture/mcp-session-recovery.ts index 7e7e576f1c..381c187f00 100644 --- a/packages/opencode/test/fixture/mcp-session-recovery.ts +++ b/packages/opencode/test/fixture/mcp-session-recovery.ts @@ -1,8 +1,11 @@ import { Client, LATEST_PROTOCOL_VERSION, StreamableHTTPClientTransport } from "@modelcontextprotocol/client" const posts: Array<{ method: string; session: string | null }> = [] +const concurrent = process.env.MCP_RECOVERY_CONCURRENT === "1" let initializeCount = 0 let pingCount = 0 +let replacementStarted!: () => void +const replacement = new Promise((resolve) => (replacementStarted = resolve)) const server = Bun.serve({ port: 0, async fetch(request) { @@ -15,6 +18,7 @@ const server = Bun.serve({ if (message.method === "initialize") { initializeCount++ + if (initializeCount === 2) replacementStarted() return Response.json( { jsonrpc: "2.0", @@ -32,7 +36,8 @@ const server = Bun.serve({ if (message.method === "notifications/initialized") return new Response(null, { status: 202 }) pingCount++ - if (pingCount === 1) return new Response("Session not found", { status: 404 }) + if (concurrent && pingCount === 2) await replacement + if (pingCount <= (concurrent ? 2 : 1)) return new Response("Session not found", { status: 404 }) return Response.json({ jsonrpc: "2.0", id: message.id, result: {} }) }, }) @@ -40,7 +45,8 @@ const client = new Client({ name: "test", version: "1" }) try { await client.connect(new StreamableHTTPClientTransport(server.url)) - await client.ping() + if (concurrent) await Promise.all([client.ping(), client.ping()]) + else await client.ping() process.stdout.write(JSON.stringify(posts)) } finally { await client.close() diff --git a/packages/opencode/test/mcp/session-recovery.test.ts b/packages/opencode/test/mcp/session-recovery.test.ts index 7011ec564b..f7c5787a4e 100644 --- a/packages/opencode/test/mcp/session-recovery.test.ts +++ b/packages/opencode/test/mcp/session-recovery.test.ts @@ -2,7 +2,7 @@ import path from "node:path" import { describe, expect, test } from "bun:test" describe("mcp session recovery", () => { - test.skip("reinitializes and retries once after a session-bound POST returns 404", async () => { + test("reinitializes and retries once after a session-bound POST returns 404", async () => { const child = Bun.spawn([process.execPath, path.join(import.meta.dir, "../fixture/mcp-session-recovery.ts")], { cwd: path.join(import.meta.dir, "../.."), stdout: "pipe", @@ -24,4 +24,24 @@ describe("mcp session recovery", () => { { method: "ping", session: "replacement" }, ]) }) + + test("retries a concurrent stale response after recovery completes", async () => { + const child = Bun.spawn([process.execPath, path.join(import.meta.dir, "../fixture/mcp-session-recovery.ts")], { + cwd: path.join(import.meta.dir, "../.."), + env: { ...process.env, MCP_RECOVERY_CONCURRENT: "1" }, + stdout: "pipe", + stderr: "pipe", + }) + const [code, stdout, stderr] = await Promise.all([ + child.exited, + Bun.readableStreamToText(child.stdout), + Bun.readableStreamToText(child.stderr), + ]) + + expect(code, stderr).toBe(0) + const posts = JSON.parse(stdout) as Array<{ method: string; session: string | null }> + expect(posts.filter((post) => post.method === "initialize").map((post) => post.session)).toEqual([null, null]) + expect(posts.filter((post) => post.method === "ping" && post.session === "expired")).toHaveLength(2) + expect(posts.filter((post) => post.method === "ping" && post.session === "replacement")).toHaveLength(2) + }) }) diff --git a/patches/@modelcontextprotocol%2Fclient@2.0.0-beta.5.patch b/patches/@modelcontextprotocol%2Fclient@2.0.0-beta.5.patch new file mode 100644 index 0000000000..4205f15831 --- /dev/null +++ b/patches/@modelcontextprotocol%2Fclient@2.0.0-beta.5.patch @@ -0,0 +1,214 @@ +diff --git a/dist/index.cjs b/dist/index.cjs +index 1c43bac25a1037416fdf2ddfb4534ba0897a2e69..7e2108326f368ccbc48897195d57e370a4553855 100644 +--- a/dist/index.cjs ++++ b/dist/index.cjs +@@ -3154,6 +3154,7 @@ var Client = class extends require_src.Protocol { + */ + async _connectPlainLegacy(transport, options) { + await super.connect(transport); ++ transport.onsessionexpired = () => this._legacyHandshake(transport, options); + if (transport.sessionId !== void 0) { + const negotiatedProtocolVersion = this._negotiatedProtocolVersion; + if (negotiatedProtocolVersion !== void 0) transport.setProtocolVersion?.(negotiatedProtocolVersion); +@@ -3170,6 +3171,7 @@ var Client = class extends require_src.Protocol { + * the handshake; its completion sets the negotiated (legacy) version. + */ + async _legacyHandshake(transport, options) { ++ transport.onsessionexpired = () => this._legacyHandshake(transport, options); + const legacyVersions = require_src.legacyProtocolVersions(this._supportedProtocolVersions); + try { + const offeredVersion = legacyVersions[0]; +@@ -3208,6 +3210,7 @@ var Client = class extends require_src.Protocol { + await super.connect(transport); + const negotiatedProtocolVersion = this._negotiatedProtocolVersion; + if (negotiatedProtocolVersion !== void 0 && transport.setProtocolVersion) transport.setProtocolVersion(negotiatedProtocolVersion); ++ if (negotiatedProtocolVersion !== void 0 && !require_src.isModernProtocolVersion(negotiatedProtocolVersion)) transport.onsessionexpired = () => this._legacyHandshake(transport, options); + return; + } + this._resetConnectionState(); +@@ -5211,10 +5214,32 @@ var StreamableHTTPClientTransport = class { + } + } + async send(message, options) { +- return this._send(message, options, false); ++ return this._send(message, options, false, 0, false); ++ } ++ async _recoverSession(expiredSessionId) { ++ if (this._sessionRecovery) return this._sessionRecovery; ++ if (!this.onsessionexpired) return false; ++ if (this._sessionId !== expiredSessionId) return true; ++ this._sessionId = void 0; ++ this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired()).then(() => true); ++ try { ++ return await this._sessionRecovery; ++ } catch (error) { ++ this._sessionId = void 0; ++ await this.close(); ++ throw error; ++ } finally { ++ this._sessionRecovery = void 0; ++ } + } +- async _send(message, options, isAuthRetry, stepUpRetries = 0) { ++ async _send(message, options, isAuthRetry, stepUpRetries = 0, isSessionRetry = false) { + try { ++ const isHandshake = Array.isArray(message) ? message.some((m) => require_src.isInitializeRequest(m)) : require_src.isInitializeRequest(message); ++ const isInitialized = Array.isArray(message) ? message.some((m) => require_src.isInitializedNotification(m)) : require_src.isInitializedNotification(message); ++ if (this._sessionRecovery && !isHandshake && !isInitialized) { ++ await this._sessionRecovery; ++ options?.requestSignal?.throwIfAborted(); ++ } + const { resumptionToken, onresumptiontoken } = options || {}; + if (resumptionToken) { + this._startOrAuthSse({ +@@ -5226,8 +5251,8 @@ var StreamableHTTPClientTransport = class { + } + const headers = await this._commonHeaders(); + this._applyBodyDerivedHeaders(headers, message); +- const isHandshake = Array.isArray(message) ? message.some((m) => require_src.isInitializeRequest(m)) : require_src.isInitializeRequest(message); + if (isHandshake) headers.delete("mcp-session-id"); ++ const requestSessionId = headers.get("mcp-session-id") || void 0; + if (options?.headers !== void 0) for (const [name, value] of Object.entries(options.headers)) { + if (RESERVED_REQUEST_HEADER_NAMES.has(name.toLowerCase())) continue; + headers.set(name, value); +@@ -5249,8 +5274,14 @@ var StreamableHTTPClientTransport = class { + signal + }; + const response = await (this._fetch ?? fetch)(this._url, init); +- if (isHandshake && response.ok) this._sessionId = response.headers.get("mcp-session-id") || void 0; ++ if (isHandshake && response.ok && (requestSessionId === void 0 || this._sessionId === requestSessionId)) this._sessionId = response.headers.get("mcp-session-id") || void 0; + if (!response.ok) { ++ if (response.status === 404 && requestSessionId && !isSessionRetry && !isInitialized) { ++ if (await this._recoverSession(requestSessionId)) { ++ options?.requestSignal?.throwIfAborted(); ++ return this._send(message, options, isAuthRetry, stepUpRetries, true); ++ } ++ } + if (response.status === 401 && this._authProvider) { + if (response.headers.has("www-authenticate")) { + const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response); +@@ -5264,7 +5295,7 @@ var StreamableHTTPClientTransport = class { + fetchFn: this._fetchWithInit + }); + await response.text?.().catch(() => {}); +- return this._send(message, options, true, stepUpRetries); ++ return this._send(message, options, true, stepUpRetries, isSessionRetry); + } + await response.text?.().catch(() => {}); + if (isAuthRetry) throw new require_src.SdkHttpError(require_src.SdkErrorCode.ClientHttpAuthentication, "Server returned 401 after re-authentication", { +@@ -5284,7 +5315,7 @@ var StreamableHTTPClientTransport = class { + statusText: response.statusText, + text + }, stepUpRetries) !== "AUTHORIZED") throw new UnauthorizedError(); +- return this._send(message, options, isAuthRetry, stepUpRetries + 1); ++ return this._send(message, options, isAuthRetry, stepUpRetries + 1, isSessionRetry); + } + } + if (response.status === 400 && typeof text === "string" && this._isModernEnvelopedRequest(message)) try { +diff --git a/dist/index.mjs b/dist/index.mjs +index 77e2389913cb5c5c2b047f95d990ab2892bef923..4b5e4ff2869189d600ca644488a7749668c39747 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -3151,6 +3151,7 @@ var Client = class extends Protocol { + */ + async _connectPlainLegacy(transport, options) { + await super.connect(transport); ++ transport.onsessionexpired = () => this._legacyHandshake(transport, options); + if (transport.sessionId !== void 0) { + const negotiatedProtocolVersion = this._negotiatedProtocolVersion; + if (negotiatedProtocolVersion !== void 0) transport.setProtocolVersion?.(negotiatedProtocolVersion); +@@ -3167,6 +3168,7 @@ var Client = class extends Protocol { + * the handshake; its completion sets the negotiated (legacy) version. + */ + async _legacyHandshake(transport, options) { ++ transport.onsessionexpired = () => this._legacyHandshake(transport, options); + const legacyVersions = legacyProtocolVersions(this._supportedProtocolVersions); + try { + const offeredVersion = legacyVersions[0]; +@@ -3205,6 +3207,7 @@ var Client = class extends Protocol { + await super.connect(transport); + const negotiatedProtocolVersion = this._negotiatedProtocolVersion; + if (negotiatedProtocolVersion !== void 0 && transport.setProtocolVersion) transport.setProtocolVersion(negotiatedProtocolVersion); ++ if (negotiatedProtocolVersion !== void 0 && !isModernProtocolVersion(negotiatedProtocolVersion)) transport.onsessionexpired = () => this._legacyHandshake(transport, options); + return; + } + this._resetConnectionState(); +@@ -5208,10 +5211,32 @@ var StreamableHTTPClientTransport = class { + } + } + async send(message, options) { +- return this._send(message, options, false); ++ return this._send(message, options, false, 0, false); ++ } ++ async _recoverSession(expiredSessionId) { ++ if (this._sessionRecovery) return this._sessionRecovery; ++ if (!this.onsessionexpired) return false; ++ if (this._sessionId !== expiredSessionId) return true; ++ this._sessionId = void 0; ++ this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired()).then(() => true); ++ try { ++ return await this._sessionRecovery; ++ } catch (error) { ++ this._sessionId = void 0; ++ await this.close(); ++ throw error; ++ } finally { ++ this._sessionRecovery = void 0; ++ } + } +- async _send(message, options, isAuthRetry, stepUpRetries = 0) { ++ async _send(message, options, isAuthRetry, stepUpRetries = 0, isSessionRetry = false) { + try { ++ const isHandshake = Array.isArray(message) ? message.some((m) => isInitializeRequest(m)) : isInitializeRequest(message); ++ const isInitialized = Array.isArray(message) ? message.some((m) => isInitializedNotification(m)) : isInitializedNotification(message); ++ if (this._sessionRecovery && !isHandshake && !isInitialized) { ++ await this._sessionRecovery; ++ options?.requestSignal?.throwIfAborted(); ++ } + const { resumptionToken, onresumptiontoken } = options || {}; + if (resumptionToken) { + this._startOrAuthSse({ +@@ -5223,8 +5248,8 @@ var StreamableHTTPClientTransport = class { + } + const headers = await this._commonHeaders(); + this._applyBodyDerivedHeaders(headers, message); +- const isHandshake = Array.isArray(message) ? message.some((m) => isInitializeRequest(m)) : isInitializeRequest(message); + if (isHandshake) headers.delete("mcp-session-id"); ++ const requestSessionId = headers.get("mcp-session-id") || void 0; + if (options?.headers !== void 0) for (const [name, value] of Object.entries(options.headers)) { + if (RESERVED_REQUEST_HEADER_NAMES.has(name.toLowerCase())) continue; + headers.set(name, value); +@@ -5246,8 +5271,14 @@ var StreamableHTTPClientTransport = class { + signal + }; + const response = await (this._fetch ?? fetch)(this._url, init); +- if (isHandshake && response.ok) this._sessionId = response.headers.get("mcp-session-id") || void 0; ++ if (isHandshake && response.ok && (requestSessionId === void 0 || this._sessionId === requestSessionId)) this._sessionId = response.headers.get("mcp-session-id") || void 0; + if (!response.ok) { ++ if (response.status === 404 && requestSessionId && !isSessionRetry && !isInitialized) { ++ if (await this._recoverSession(requestSessionId)) { ++ options?.requestSignal?.throwIfAborted(); ++ return this._send(message, options, isAuthRetry, stepUpRetries, true); ++ } ++ } + if (response.status === 401 && this._authProvider) { + if (response.headers.has("www-authenticate")) { + const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response); +@@ -5261,7 +5292,7 @@ var StreamableHTTPClientTransport = class { + fetchFn: this._fetchWithInit + }); + await response.text?.().catch(() => {}); +- return this._send(message, options, true, stepUpRetries); ++ return this._send(message, options, true, stepUpRetries, isSessionRetry); + } + await response.text?.().catch(() => {}); + if (isAuthRetry) throw new SdkHttpError(SdkErrorCode.ClientHttpAuthentication, "Server returned 401 after re-authentication", { +@@ -5281,7 +5312,7 @@ var StreamableHTTPClientTransport = class { + statusText: response.statusText, + text + }, stepUpRetries) !== "AUTHORIZED") throw new UnauthorizedError(); +- return this._send(message, options, isAuthRetry, stepUpRetries + 1); ++ return this._send(message, options, isAuthRetry, stepUpRetries + 1, isSessionRetry); + } + } + if (response.status === 400 && typeof text === "string" && this._isModernEnvelopedRequest(message)) try { From c8487bac54d220b085b86b344cb15c61d67f215d Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 28 Jul 2026 05:50:46 +0000 Subject: [PATCH 052/129] 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 ce982c7ac4..1b85cf68b2 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-YH/hjSpK7t5RKE2ie6l+EDKaXKoqVXR11ihGXK957DQ=", - "aarch64-linux": "sha256-fJztjpptVyHStsCWxaaAaQnfxu4Uh2fv79qCag1pokg=", - "aarch64-darwin": "sha256-C9Mno6ts6f19iwPAiO9299CMpK7/dmSL5JS9JSTAmKw=", - "x86_64-darwin": "sha256-zcfjXO73bPK1JtcxekDNyGPwIkj3LMITzAB0hq2g/Ro=" + "x86_64-linux": "sha256-RFek0QoEEjsgbqmTE/SxQAmPtYyzs0IPR2ugFn5Okrs=", + "aarch64-linux": "sha256-BmAxapY1YrAFn7mVq3/6A9+6Au5UIvSqBboHMkyJH3I=", + "aarch64-darwin": "sha256-Sx3bGWQqLlgoa/RudJxanjSzhFRNklckT2ffnO2I5F4=", + "x86_64-darwin": "sha256-CMOhiisHNowg06qadvgg4K+60zrynglwiT0qKYQ4NiA=" } } From 8021dbd80f7176e5537564af4fcbb90c8c0ad012 Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 28 Jul 2026 06:07:51 +0000 Subject: [PATCH 053/129] sync release versions for v1.18.8 --- bun.lock | 56 ++++++++++----------- packages/app/package.json | 2 +- packages/cli/package.json | 2 +- packages/codemode/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/console/support/package.json | 2 +- packages/core/package.json | 2 +- packages/desktop/package.json | 2 +- packages/effect-drizzle-sqlite/package.json | 2 +- packages/effect-sqlite-node/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/function/package.json | 2 +- packages/http-recorder/package.json | 2 +- packages/llm/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/server/package.json | 2 +- packages/session-ui/package.json | 2 +- packages/slack/package.json | 2 +- packages/stats/app/package.json | 2 +- packages/stats/core/package.json | 2 +- packages/stats/server/package.json | 2 +- packages/tui/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 30 files changed, 57 insertions(+), 57 deletions(-) diff --git a/bun.lock b/bun.lock index de6eb95f9c..b4571f1314 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", @@ -96,7 +96,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.18.7", + "version": "1.18.8", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -144,7 +144,7 @@ }, "packages/codemode": { "name": "@opencode-ai/codemode", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "acorn": "8.15.0", "effect": "catalog:", @@ -158,7 +158,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -194,7 +194,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -221,7 +221,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -243,7 +243,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -267,7 +267,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -287,7 +287,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.18.7", + "version": "1.18.8", "bin": { "opencode": "./bin/opencode", }, @@ -381,7 +381,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@zip.js/zip.js": "2.7.62", "effect": "catalog:", @@ -435,7 +435,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -449,7 +449,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "effect": "catalog:", }, @@ -461,7 +461,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -493,7 +493,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -509,7 +509,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83", @@ -540,7 +540,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", @@ -559,7 +559,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.18.7", + "version": "1.18.8", "bin": { "opencode": "./bin/opencode", }, @@ -691,7 +691,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "workspace:*", @@ -767,7 +767,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "cross-spawn": "catalog:", }, @@ -782,7 +782,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", @@ -797,7 +797,7 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", @@ -842,7 +842,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -855,7 +855,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@ibm/plex": "6.4.1", "@kobalte/core": "catalog:", @@ -889,7 +889,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -908,7 +908,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -950,7 +950,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -977,7 +977,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1028,7 +1028,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.18.7", + "version": "1.18.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 8341a2b268..73840b2dbf 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.18.7", + "version": "1.18.8", "description": "", "type": "module", "exports": { diff --git a/packages/cli/package.json b/packages/cli/package.json index c93fa66255..45271e869c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/cli", - "version": "1.18.7", + "version": "1.18.8", "type": "module", "license": "MIT", "bin": { diff --git a/packages/codemode/package.json b/packages/codemode/package.json index acdaca0ec5..51df6e53a5 100644 --- a/packages/codemode/package.json +++ b/packages/codemode/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/codemode", - "version": "1.18.7", + "version": "1.18.8", "description": "Effect-native confined code execution over schema-described tools", "private": true, "type": "module", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index dc2a03a1c5..446c3d0854 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.18.7", + "version": "1.18.8", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 140c32ecb4..f5a1bbb72a 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.18.7", + "version": "1.18.8", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 9101f0ea46..937c0cf538 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.18.7", + "version": "1.18.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 939824a4db..531b0ec651 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.18.7", + "version": "1.18.8", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/console/support/package.json b/packages/console/support/package.json index a6c4d2ec78..a556e236bb 100644 --- a/packages/console/support/package.json +++ b/packages/console/support/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-support", - "version": "1.18.7", + "version": "1.18.8", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index d9da0f892e..c1635e6323 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.7", + "version": "1.18.8", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 4ac36046a1..2f3b90ae9b 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.18.7", + "version": "1.18.8", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index 341291dd26..a3e9ea8814 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.7", + "version": "1.18.8", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/effect-sqlite-node/package.json b/packages/effect-sqlite-node/package.json index d14decf085..507ad6769e 100644 --- a/packages/effect-sqlite-node/package.json +++ b/packages/effect-sqlite-node/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.7", + "version": "1.18.8", "name": "@opencode-ai/effect-sqlite-node", "type": "module", "license": "MIT", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 7c20f5e299..7a4c65783f 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.18.7", + "version": "1.18.8", "private": true, "type": "module", "license": "MIT", diff --git a/packages/function/package.json b/packages/function/package.json index 1c1bcf83ec..e7f8516161 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.18.7", + "version": "1.18.8", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index 980c2b24a9..1eaf4333fc 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.7", + "version": "1.18.8", "name": "@opencode-ai/http-recorder", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "type": "module", diff --git a/packages/llm/package.json b/packages/llm/package.json index cd62b1eb13..6d3a826e59 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.7", + "version": "1.18.8", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 6544ba09e2..293a678e9b 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.7", + "version": "1.18.8", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 058803a252..fefab272f5 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.18.7", + "version": "1.18.8", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 422f5ac849..8e8c19d73b 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.18.7", + "version": "1.18.8", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index d4ffebe4fd..4532b2e37b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/server", - "version": "1.18.7", + "version": "1.18.8", "private": true, "type": "module", "license": "MIT", diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index fe07ed5e45..879f254df1 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/session-ui", - "version": "1.18.7", + "version": "1.18.8", "private": true, "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index 85d1ede184..5636890bb2 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.18.7", + "version": "1.18.8", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json index c32ac327d7..aceecc49d5 100644 --- a/packages/stats/app/package.json +++ b/packages/stats/app/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-app", - "version": "1.18.7", + "version": "1.18.8", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/core/package.json b/packages/stats/core/package.json index d1e74ec3d8..73de8bbced 100644 --- a/packages/stats/core/package.json +++ b/packages/stats/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-core", - "version": "1.18.7", + "version": "1.18.8", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/server/package.json b/packages/stats/server/package.json index 8f2f05e5c3..af0f57d187 100644 --- a/packages/stats/server/package.json +++ b/packages/stats/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-server", - "version": "1.18.7", + "version": "1.18.8", "private": true, "type": "module", "license": "MIT", diff --git a/packages/tui/package.json b/packages/tui/package.json index 4f9850eba8..8207147917 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/tui", - "version": "1.18.7", + "version": "1.18.8", "private": true, "type": "module", "license": "MIT", diff --git a/packages/ui/package.json b/packages/ui/package.json index d5022aeb48..e5e7a15a1c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.18.7", + "version": "1.18.8", "type": "module", "license": "MIT", "repository": { diff --git a/packages/web/package.json b/packages/web/package.json index 1966ee63fb..963e9dfc5f 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.18.7", + "version": "1.18.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 6a53c96c5b..a5948bb436 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.18.7", + "version": "1.18.8", "publisher": "sst-dev", "repository": { "type": "git", From d46be565aa7197446a408adf2dbb531d02d4295c Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:02:26 +0800 Subject: [PATCH 054/129] refactor(app): thin new session composition (#39227) --- packages/app/src/components/session/index.ts | 1 - .../session/session-new-design-view.tsx | 16 - packages/app/src/pages/new-session.tsx | 291 ++---------------- .../new-session-draft-controller.ts | 64 ++++ .../pages/new-session/new-session-view.tsx | 165 ++++++++++ .../new-session-workspace-controller.test.ts | 43 +++ .../new-session-workspace-controller.ts | 77 +++++ .../new-session/use-new-session-commands.tsx | 44 +++ 8 files changed, 418 insertions(+), 283 deletions(-) delete mode 100644 packages/app/src/components/session/session-new-design-view.tsx create mode 100644 packages/app/src/pages/new-session/new-session-draft-controller.ts create mode 100644 packages/app/src/pages/new-session/new-session-view.tsx create mode 100644 packages/app/src/pages/new-session/new-session-workspace-controller.test.ts create mode 100644 packages/app/src/pages/new-session/new-session-workspace-controller.ts create mode 100644 packages/app/src/pages/new-session/use-new-session-commands.tsx diff --git a/packages/app/src/components/session/index.ts b/packages/app/src/components/session/index.ts index 9e44cea17b..c4fdb2ff38 100644 --- a/packages/app/src/components/session/index.ts +++ b/packages/app/src/components/session/index.ts @@ -4,4 +4,3 @@ export { SortableTab, FileVisual } from "./session-sortable-tab" export { SortableTabV2 } from "./session-sortable-tab-v2" export { SortableTerminalTab } from "./session-sortable-terminal-tab" export { NewSessionView } from "./session-new-view" -export { NewSessionDesignView } from "./session-new-design-view" diff --git a/packages/app/src/components/session/session-new-design-view.tsx b/packages/app/src/components/session/session-new-design-view.tsx deleted file mode 100644 index a324c64fa6..0000000000 --- a/packages/app/src/components/session/session-new-design-view.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { JSX } from "solid-js" -import { WordmarkV2 } from "@opencode-ai/ui/v2/wordmark-v2" -import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout" - -export function NewSessionDesignView(props: { children: JSX.Element }) { - return ( -
-
-
- -
{props.children}
-
-
-
- ) -} diff --git a/packages/app/src/pages/new-session.tsx b/packages/app/src/pages/new-session.tsx index 570f04463e..7c7b89f589 100644 --- a/packages/app/src/pages/new-session.tsx +++ b/packages/app/src/pages/new-session.tsx @@ -1,290 +1,49 @@ -import { Show, createEffect, createMemo, createResource, createSignal, onCleanup, untrack } from "solid-js" -import { createStore } from "solid-js/store" -import { Portal } from "solid-js/web" -import { useSearchParams } from "@solidjs/router" -import { Tooltip } from "@opencode-ai/ui/tooltip" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" -import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" -import { NewSessionDesignView } from "@/components/session" -import { PromptInputV2Composer, usePromptInputV2Controller } from "@/components/prompt-input-v2" -import { StatusPopoverV2 } from "@/components/status-popover" -import { - PromptProjectAddButton, - PromptProjectSelector, - createPromptProjectController, -} from "@/components/prompt-project-selector" -import { useComments } from "@/context/comments" -import { usePrompt } from "@/context/prompt" -import { useSDK } from "@/context/sdk" -import { useSync } from "@/context/sync" -import { useServerSync } from "@/context/server-sync" -import { useLanguage } from "@/context/language" -import { useSettings } from "@/context/settings" -import { createPromptInputController, createPromptProjectControls } from "@/pages/session/composer" -import { useSessionKey } from "@/pages/session/session-layout" -import { useComposerCommands } from "@/pages/session/use-composer-commands" -import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout" -import { PromptGitStatus, PromptWorkspaceSelector } from "@/components/prompt-workspace-selector" +import { createPromptProjectController } from "@/components/prompt-project-selector" import { useTitlebarRightMount } from "@/components/titlebar" -import { useCommand } from "@/context/command" -import { useProviders } from "@/hooks/use-providers" -import { useSettingsCommand } from "@/components/settings-dialog" -import { Persist, persisted } from "@/utils/persist" -import createPresence from "solid-presence" -import { useLocal } from "@/context/local" -import { createPromptModelSelection } from "@/pages/session/composer/prompt-model-selection" +import { useSettings } from "@/context/settings" +import { createEffect, createResource } from "solid-js" +import { createNewSessionDraftController } from "./new-session/new-session-draft-controller" +import { NewSessionStatus, NewSessionView } from "./new-session/new-session-view" +import { createNewSessionWorkspaceController } from "./new-session/new-session-workspace-controller" +import { useNewSessionCommands } from "./new-session/use-new-session-commands" -const workspaceBarEnabled = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" -const providerTipDismissalDuration = 30 * 24 * 60 * 60 * 1000 -const providerTipExitDuration = 250 - -/** - * The `/new-session` draft page. Unlike `session.tsx`, this only renders the prompt - * composer for a brand-new session — no terminal, review pane, file tree, or message - * timeline. Submitting promotes the draft into a real session (see prompt-input/submit). - */ +/** The draft-only V2 session page. Submitting promotes the draft into a real session. */ export default function NewSessionPage() { - const prompt = usePrompt() - const sdk = useSDK() - const sync = useSync() - const serverSync = useServerSync() - const comments = useComments() - const language = useLanguage() const settings = useSettings() - const dialog = useDialog() - const command = useCommand() - const providers = useProviders(() => sdk().directory) - const openProviders = () => { - void import("@/components/dialog-connect-provider").then(({ DialogConnectProvider }) => { - void dialog.show(() => sdk().directory} />) - }) - } - useSettingsCommand() - const route = useSessionKey() - const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>() - const local = useLocal() - const model = createPromptModelSelection({ agent: local.agent.current }) - - useComposerCommands({ model }) - - const inputController = createPromptInputController({ - sessionKey: route.sessionKey, - sessionID: () => route.params.id, - queryOptions: serverSync().queryOptions, - model, - }) - const projectControls = createPromptProjectControls() - - const [store, setStore] = createStore<{ worktree?: string }>({}) const rightMount = useTitlebarRightMount() - - const showWorkspaceBar = createMemo(() => workspaceBarEnabled && sync().project?.vcs === "git") - const newSessionWorktree = createMemo(() => { - if (!showWorkspaceBar()) return "main" - if (store.worktree) return store.worktree - const project = sync().project - if (project && sdk().directory !== project.worktree) return sdk().directory - return "main" + const workspace = createNewSessionWorkspaceController() + const draft = createNewSessionDraftController({ + worktree: workspace.selection.value, + resetWorktree: workspace.selection.reset, }) - const projectRoot = createMemo(() => sync().project?.worktree ?? sdk().directory) - const localBranch = createMemo(() => serverSync().child(projectRoot())[0].vcs?.branch) - const selectedBranch = createMemo(() => { - const worktree = newSessionWorktree() - if (worktree === "main" || worktree === "create") return localBranch() - return serverSync().child(worktree)[0].vcs?.branch ?? localBranch() + const project = createPromptProjectController({ + controls: draft.project.controls, + onDone: draft.input.restoreFocus, }) - const promptInputV2Controller = usePromptInputV2Controller({ - get controls() { - return inputController() + useNewSessionCommands({ + restoreFocus: draft.input.restoreFocus, + project: { + empty: project.empty, + open: () => project.setOpen(true), }, - get newSessionWorktree() { - return newSessionWorktree() - }, - onNewSessionWorktreeReset: () => setStore("worktree", undefined), - onSubmit: () => comments.clear(), }) - const projectController = createPromptProjectController({ - controls: projectControls, - onDone: promptInputV2Controller.restoreFocus, - }) - - command.register("new-session", () => [ - { - id: "command.palette", - title: language.t("command.palette"), - hidden: true, - onSelect: async () => { - const { DialogSelectFile } = await import("@/components/dialog-select-file") - void dialog.show(() => ) - }, - }, - { - id: "input.focus", - title: language.t("command.input.focus"), - category: language.t("command.category.view"), - keybind: "ctrl+l", - onSelect: () => promptInputV2Controller.restoreFocus(), - }, - { - id: "project.select", - title: language.t("session.new.project.search"), - category: language.t("command.category.project"), - keybind: "mod+shift+o", - disabled: projectController.empty(), - onSelect: () => projectController.setOpen(true), - }, - ]) - createEffect(() => { - if (!prompt.ready()) return - untrack(() => { - const text = searchParams.prompt - if (!text) return - prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) - setSearchParams({ ...searchParams, prompt: undefined }) - }) + if (!draft.prompt.ready()) return + draft.input.restoreFocus() }) - - createEffect(() => { - if (!prompt.ready()) return - promptInputV2Controller.restoreFocus() - }) - const ready = Promise.resolve() const [suspendUntilPromptReady] = createResource( - () => prompt.ready.promise ?? ready, + () => draft.prompt.readyPromise() ?? ready, (promise) => promise.then(() => true), ) return (
{suspendUntilPromptReady()} - - {(mount) => ( - - - - - - - - )} - +
-
-
- -
-
- - - - - -
- - } - > - - setStore( - "worktree", - value === "main" && sync().project?.worktree !== sdk().directory - ? sync().project?.worktree - : value, - ) - } - onDone={promptInputV2Controller.restoreFocus} - /> - -
-
-
- {/**/} -
-
- serverSync().child(sdk().directory)[0].provider_ready} - connected={() => providers.paid().length > 0} - openProviders={openProviders} - /> -
-
+
) } - -function ProviderTip(props: { ready: () => boolean; connected: () => boolean; openProviders: () => void }) { - const language = useLanguage() - const [persistedState, setPersistedState, , persistedReady] = persisted( - Persist.global("new-session.provider-tip"), - createStore({ dismissedAt: 0 }), - ) - const visible = createMemo( - () => - props.ready() && - persistedReady() && - !props.connected() && - Date.now() - persistedState.dismissedAt >= providerTipDismissalDuration, - ) - - function dismiss() { - setPersistedState("dismissedAt", Date.now()) - } - - const [ref, setRef] = createSignal() - const presence = createPresence({ - show: () => visible(), - element: () => ref() ?? null, - }) - - return ( - -
-
- - - - -
-
-
- ) -} diff --git a/packages/app/src/pages/new-session/new-session-draft-controller.ts b/packages/app/src/pages/new-session/new-session-draft-controller.ts new file mode 100644 index 0000000000..bf22834e48 --- /dev/null +++ b/packages/app/src/pages/new-session/new-session-draft-controller.ts @@ -0,0 +1,64 @@ +import { useSearchParams } from "@solidjs/router" +import { createEffect, untrack } from "solid-js" +import { usePromptInputV2Controller } from "@/components/prompt-input-v2" +import { useComments } from "@/context/comments" +import { useLocal } from "@/context/local" +import { usePrompt } from "@/context/prompt" +import { useServerSync } from "@/context/server-sync" +import { createPromptInputController, createPromptProjectControls } from "@/pages/session/composer" +import { createPromptModelSelection } from "@/pages/session/composer/prompt-model-selection" +import { useSessionKey } from "@/pages/session/session-layout" +import { useComposerCommands } from "@/pages/session/use-composer-commands" + +export function createNewSessionDraftController(workspace: { worktree: () => string; resetWorktree: () => void }) { + const prompt = usePrompt() + const serverSync = useServerSync() + const comments = useComments() + const local = useLocal() + const route = useSessionKey() + const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>() + const model = createPromptModelSelection({ agent: () => local.agent.current() }) + + useComposerCommands({ model }) + + const controls = createPromptInputController({ + sessionKey: route.sessionKey, + sessionID: () => route.params.id, + queryOptions: serverSync().queryOptions, + model, + }) + const projectControls = createPromptProjectControls() + const input = usePromptInputV2Controller({ + get controls() { + return controls() + }, + get newSessionWorktree() { + return workspace.worktree() + }, + onNewSessionWorktreeReset: workspace.resetWorktree, + onSubmit: comments.clear, + }) + + createEffect(() => { + if (!prompt.ready()) return + untrack(() => { + const text = searchParams.prompt + if (!text) return + prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) + setSearchParams({ ...searchParams, prompt: undefined }) + }) + }) + + return { + input, + prompt: { + ready: prompt.ready, + readyPromise: () => prompt.ready.promise, + }, + project: { + controls: projectControls, + }, + } +} + +export type NewSessionDraftController = ReturnType diff --git a/packages/app/src/pages/new-session/new-session-view.tsx b/packages/app/src/pages/new-session/new-session-view.tsx new file mode 100644 index 0000000000..3117bf800a --- /dev/null +++ b/packages/app/src/pages/new-session/new-session-view.tsx @@ -0,0 +1,165 @@ +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Tooltip } from "@opencode-ai/ui/tooltip" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { WordmarkV2 } from "@opencode-ai/ui/v2/wordmark-v2" +import { Show, createMemo, createSignal, type Accessor } from "solid-js" +import { createStore } from "solid-js/store" +import { Portal } from "solid-js/web" +import createPresence from "solid-presence" +import { PromptInputV2Composer } from "@/components/prompt-input-v2" +import { PromptGitStatus, PromptWorkspaceSelector } from "@/components/prompt-workspace-selector" +import { + PromptProjectAddButton, + PromptProjectSelector, + type PromptProjectController, +} from "@/components/prompt-project-selector" +import { StatusPopoverV2 } from "@/components/status-popover" +import { useLanguage } from "@/context/language" +import { useSDK } from "@/context/sdk" +import { useServerSync } from "@/context/server-sync" +import { useProviders } from "@/hooks/use-providers" +import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout" +import { Persist, persisted } from "@/utils/persist" +import type { NewSessionDraftController } from "./new-session-draft-controller" +import type { NewSessionWorkspaceController } from "./new-session-workspace-controller" + +const providerTipDismissalDuration = 30 * 24 * 60 * 60 * 1000 + +export function NewSessionView(props: { + input: NewSessionDraftController["input"] + project: PromptProjectController + workspace: NewSessionWorkspaceController +}) { + return ( +
+
+
+
+ +
+ + + + + +
+ + + } + > + + +
+
+
+
+
+ +
+
+ ) +} + +export function NewSessionStatus(props: { mount: Accessor; visible: Accessor }) { + const language = useLanguage() + + return ( + + {(mount) => ( + + + + + + + + )} + + ) +} + +function ProviderTip() { + const language = useLanguage() + const dialog = useDialog() + const sdk = useSDK() + const serverSync = useServerSync() + const providers = useProviders(() => sdk().directory) + const [persistedState, setPersistedState, , persistedReady] = persisted( + Persist.global("new-session.provider-tip"), + createStore({ dismissedAt: 0 }), + ) + const visible = createMemo( + () => + serverSync().child(sdk().directory)[0].provider_ready && + persistedReady() && + providers.paid().length === 0 && + Date.now() - persistedState.dismissedAt >= providerTipDismissalDuration, + ) + const [ref, setRef] = createSignal() + const presence = createPresence({ + show: visible, + element: () => ref() ?? null, + }) + const openProviders = () => { + void import("@/components/dialog-connect-provider").then(({ DialogConnectProvider }) => { + void dialog.show(() => sdk().directory} />) + }) + } + + return ( + +
+
+ + + + +
+
+
+ ) +} diff --git a/packages/app/src/pages/new-session/new-session-workspace-controller.test.ts b/packages/app/src/pages/new-session/new-session-workspace-controller.test.ts new file mode 100644 index 0000000000..2a79ae77fa --- /dev/null +++ b/packages/app/src/pages/new-session/new-session-workspace-controller.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, test } from "bun:test" +import { + normalizeNewSessionWorktree, + resolveNewSessionBranch, + resolveNewSessionWorktree, +} from "./new-session-workspace-controller" + +describe("new session workspace selection", () => { + test("uses main when the workspace bar is unavailable", () => { + expect( + resolveNewSessionWorktree({ + enabled: false, + selected: "/project/feature", + directory: "/project/feature", + projectWorktree: "/project", + }), + ).toBe("main") + }) + + test("derives an existing worktree from the current directory", () => { + expect( + resolveNewSessionWorktree({ enabled: true, directory: "/project/feature", projectWorktree: "/project" }), + ).toBe("/project/feature") + expect(resolveNewSessionWorktree({ enabled: true, directory: "/project", projectWorktree: "/project" })).toBe( + "main", + ) + }) + + test("normalizes main to the project root outside the main worktree", () => { + expect(normalizeNewSessionWorktree("main", "/project/feature", "/project")).toBe("/project") + expect(normalizeNewSessionWorktree("main", "/project", "/project")).toBe("main") + }) + + test("falls back to the local branch for main, create, and unknown worktrees", () => { + const branch = (worktree: string) => (worktree === "/project/feature" ? "feature" : undefined) + expect(resolveNewSessionBranch({ worktree: "main", local: "dev", worktreeBranch: branch })).toBe("dev") + expect(resolveNewSessionBranch({ worktree: "create", local: "dev", worktreeBranch: branch })).toBe("dev") + expect(resolveNewSessionBranch({ worktree: "/project/feature", local: "dev", worktreeBranch: branch })).toBe( + "feature", + ) + expect(resolveNewSessionBranch({ worktree: "/missing", local: "dev", worktreeBranch: branch })).toBe("dev") + }) +}) diff --git a/packages/app/src/pages/new-session/new-session-workspace-controller.ts b/packages/app/src/pages/new-session/new-session-workspace-controller.ts new file mode 100644 index 0000000000..f3fc9b2708 --- /dev/null +++ b/packages/app/src/pages/new-session/new-session-workspace-controller.ts @@ -0,0 +1,77 @@ +import { createMemo, createSignal } from "solid-js" +import { useSDK } from "@/context/sdk" +import { useServerSync } from "@/context/server-sync" +import { useSync } from "@/context/sync" + +const workspaceBarEnabled = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" + +export function resolveNewSessionWorktree(input: { + enabled: boolean + selected?: string + directory: string + projectWorktree?: string +}) { + if (!input.enabled) return "main" + if (input.selected) return input.selected + if (input.projectWorktree && input.directory !== input.projectWorktree) return input.directory + return "main" +} + +export function normalizeNewSessionWorktree(value: string, directory: string, projectWorktree?: string) { + if (value === "main" && projectWorktree !== directory) return projectWorktree + return value +} + +export function resolveNewSessionBranch(input: { + worktree: string + local?: string + worktreeBranch: (worktree: string) => string | undefined +}) { + if (input.worktree === "main" || input.worktree === "create") return input.local + return input.worktreeBranch(input.worktree) ?? input.local +} + +export function createNewSessionWorkspaceController() { + const sdk = useSDK() + const sync = useSync() + const serverSync = useServerSync() + const [worktree, setWorktree] = createSignal() + const visible = createMemo(() => workspaceBarEnabled && sync().project?.vcs === "git") + const value = createMemo(() => + resolveNewSessionWorktree({ + enabled: visible(), + selected: worktree(), + directory: sdk().directory, + projectWorktree: sync().project?.worktree, + }), + ) + const projectRoot = createMemo(() => sync().project?.worktree ?? sdk().directory) + const localBranch = createMemo(() => serverSync().child(projectRoot())[0].vcs?.branch) + const branch = createMemo(() => + resolveNewSessionBranch({ + worktree: value(), + local: localBranch(), + worktreeBranch: (worktree) => serverSync().child(worktree)[0].vcs?.branch, + }), + ) + + return { + selection: { + value, + reset: () => setWorktree(), + set: (worktree: string) => + setWorktree(normalizeNewSessionWorktree(worktree, sdk().directory, sync().project?.worktree)), + }, + project: { + root: projectRoot, + workspaces: () => sync().project?.sandboxes ?? [], + git: () => sync().project?.vcs === "git", + }, + bar: { + visible, + branch, + }, + } +} + +export type NewSessionWorkspaceController = ReturnType diff --git a/packages/app/src/pages/new-session/use-new-session-commands.tsx b/packages/app/src/pages/new-session/use-new-session-commands.tsx new file mode 100644 index 0000000000..a0d835f97f --- /dev/null +++ b/packages/app/src/pages/new-session/use-new-session-commands.tsx @@ -0,0 +1,44 @@ +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useSettingsCommand } from "@/components/settings-dialog" +import { useCommand } from "@/context/command" +import { useLanguage } from "@/context/language" + +export function useNewSessionCommands(input: { + restoreFocus: () => void + project: { + empty: () => boolean + open: () => void + } +}) { + const command = useCommand() + const dialog = useDialog() + const language = useLanguage() + + useSettingsCommand() + command.register("new-session", () => [ + { + id: "command.palette", + title: language.t("command.palette"), + hidden: true, + onSelect: async () => { + const { DialogSelectFile } = await import("@/components/dialog-select-file") + void dialog.show(() => ) + }, + }, + { + id: "input.focus", + title: language.t("command.input.focus"), + category: language.t("command.category.view"), + keybind: "ctrl+l", + onSelect: input.restoreFocus, + }, + { + id: "project.select", + title: language.t("session.new.project.search"), + category: language.t("command.category.project"), + keybind: "mod+shift+o", + disabled: input.project.empty(), + onSelect: input.project.open, + }, + ]) +} From 7dae9a10839504942ec405e26377527b9472b857 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 28 Jul 2026 07:03:56 +0000 Subject: [PATCH 055/129] chore: generate --- packages/app/src/pages/new-session/new-session-view.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/app/src/pages/new-session/new-session-view.tsx b/packages/app/src/pages/new-session/new-session-view.tsx index 3117bf800a..84c671e140 100644 --- a/packages/app/src/pages/new-session/new-session-view.tsx +++ b/packages/app/src/pages/new-session/new-session-view.tsx @@ -51,10 +51,7 @@ export function NewSessionView(props: { + } > Date: Tue, 28 Jul 2026 09:35:57 +0200 Subject: [PATCH 056/129] feat(desktop): collapse model provider sections (#39283) --- .../app/src/components/settings-v2/models.tsx | 117 +++++++++++++----- .../components/settings-v2/settings-v2.css | 58 ++++++++- 2 files changed, 139 insertions(+), 36 deletions(-) diff --git a/packages/app/src/components/settings-v2/models.tsx b/packages/app/src/components/settings-v2/models.tsx index a3f058670e..482e8cbffb 100644 --- a/packages/app/src/components/settings-v2/models.tsx +++ b/packages/app/src/components/settings-v2/models.tsx @@ -5,9 +5,12 @@ import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" import { type Component, For, Show } from "solid-js" +import { createStore } from "solid-js/store" import { useLanguage } from "@/context/language" import { useModels } from "@/context/models" +import { useServerSDK } from "@/context/server-sdk" import { popularProviders } from "@/hooks/use-providers" +import { Persist, persisted } from "@/utils/persist" import { SettingsListV2 } from "./parts/list" import { SettingsRowV2 } from "./parts/row" import "./settings-v2.css" @@ -19,6 +22,11 @@ const PROVIDER_ICON_SIZE = 16 export const SettingsModelsV2: Component = () => { const language = useLanguage() const models = useModels() + const serverSdk = useServerSDK() + const [store, setStore] = persisted( + Persist.serverGlobal(serverSdk().scope, "settings-v2.models.providers"), + createStore({ collapsed: {} as Record }), + ) const list = useFilteredList({ items: (_filter) => models.list(), @@ -94,41 +102,82 @@ export const SettingsModelsV2: Component = () => { } > - {(group) => ( -
-
- -

{group.items[0].provider.name}

+ {(group) => { + const searching = () => list.filter().length > 0 + const expanded = () => searching() || !store.collapsed[group.category] + + return ( +
+

+ +

+ + + + {(item) => { + const key = { providerID: item.provider.id, modelID: item.id } + return ( + +
+ { + models.setVisibility(key, checked) + }} + hideLabel + > + {item.name} + +
+
+ ) + }} +
+
+
- - - {(item) => { - const key = { providerID: item.provider.id, modelID: item.id } - return ( - -
- { - models.setVisibility(key, checked) - }} - hideLabel - > - {item.name} - -
-
- ) - }} -
-
-
- )} + ) + }} diff --git a/packages/app/src/components/settings-v2/settings-v2.css b/packages/app/src/components/settings-v2/settings-v2.css index 64a55ea3f4..c4d47344a2 100644 --- a/packages/app/src/components/settings-v2/settings-v2.css +++ b/packages/app/src/components/settings-v2/settings-v2.css @@ -373,14 +373,64 @@ } .settings-v2-models { - gap: 24px; + gap: 12px; +} + +.settings-v2-models .settings-v2-section { + gap: 12px; +} + +.settings-v2-models .settings-v2-section[data-expanded] { + padding-bottom: 12px; } .settings-v2-models-group-header { + display: flex; + align-items: center; + height: 28px; +} + +.settings-v2-models-group-trigger { + display: flex; + align-items: center; + gap: 4px; + height: 28px; + padding: 4px 8px 4px 4px; + border: 0; + border-radius: 4px; + background: transparent; + color: var(--v2-text-text-base); +} + +.settings-v2-models-group-trigger:focus-visible { + outline: 2px solid var(--v2-border-border-focus); + outline-offset: 2px; +} + +@media (hover: hover) { + .settings-v2-models-group-trigger:not(:disabled):hover { + background-color: var(--v2-background-bg-layer-02); + } +} + +.settings-v2-models-group-trigger:disabled { + cursor: default; +} + +.settings-v2-models-group-chevron { + display: flex; + width: 20px; + height: 20px; + flex-shrink: 0; + align-items: center; + justify-content: center; + color: var(--v2-icon-icon-muted); +} + +.settings-v2-models-group-label { display: flex; align-items: center; gap: 8px; - padding-bottom: 8px; } .settings-v2-models .settings-v2-section-title { @@ -394,6 +444,10 @@ color: var(--v2-icon-icon-base); } +.settings-v2-models [data-component="settings-v2-list"] { + padding-inline: 16px; +} + .settings-v2-models .settings-v2-section-title + [data-component="settings-v2-list"] { margin-top: 0; } From 8c81f9a40b0eb8ccef911a033ea685e3233ad461 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:12:59 +0800 Subject: [PATCH 057/129] refactor(app): extract v2 settings controllers (#39228) --- .../general-controller-behavior.ts | 70 ++ .../settings-v2/general-controllers.test.ts | 53 ++ .../settings-v2/general-controllers.ts | 173 +++++ .../src/components/settings-v2/general.tsx | 666 +++++++----------- 4 files changed, 553 insertions(+), 409 deletions(-) create mode 100644 packages/app/src/components/settings-v2/general-controller-behavior.ts create mode 100644 packages/app/src/components/settings-v2/general-controllers.test.ts create mode 100644 packages/app/src/components/settings-v2/general-controllers.ts diff --git a/packages/app/src/components/settings-v2/general-controller-behavior.ts b/packages/app/src/components/settings-v2/general-controller-behavior.ts new file mode 100644 index 0000000000..64bd4c1187 --- /dev/null +++ b/packages/app/src/components/settings-v2/general-controller-behavior.ts @@ -0,0 +1,70 @@ +import { onCleanup } from "solid-js" + +export type ShellOption = { + path: string + name: string + acceptable: boolean +} + +export type ShellSelectOption = { + id: string + value: string + name: string + terminalOnly: boolean +} + +export function createShellOptions(input: { shells: ShellOption[]; current: string | undefined }) { + const counts = input.shells.reduce((result, shell) => { + result.set(shell.name, (result.get(shell.name) ?? 0) + 1) + return result + }, new Map()) + const options: ShellSelectOption[] = [ + { id: "auto", value: "", name: "", terminalOnly: false }, + ...input.shells.map((shell) => { + const ambiguous = (counts.get(shell.name) ?? 0) > 1 + const name = ambiguous ? shell.path : shell.name + return { + id: shell.path, + value: ambiguous ? shell.path : shell.name, + name, + terminalOnly: !shell.acceptable, + } + }), + ] + if (input.current && !options.some((option) => option.value === input.current)) { + options.push({ id: input.current, value: input.current, name: input.current, terminalOnly: false }) + } + return options +} + +export function createSoundPreviewController(player: (id: string | undefined) => Promise<(() => void) | undefined>) { + let cleanup: (() => void) | undefined + let timeout: ReturnType | undefined + let run = 0 + + const stop = () => { + run += 1 + cleanup?.() + clearTimeout(timeout) + cleanup = undefined + timeout = undefined + } + const play = (id: string | undefined) => { + stop() + if (!id) return + const current = ++run + timeout = setTimeout(() => { + timeout = undefined + void player(id).then((next) => { + if (run === current) { + cleanup = next + return + } + next?.() + }) + }, 100) + } + + onCleanup(stop) + return { play, stop } +} diff --git a/packages/app/src/components/settings-v2/general-controllers.test.ts b/packages/app/src/components/settings-v2/general-controllers.test.ts new file mode 100644 index 0000000000..09c8d21bd5 --- /dev/null +++ b/packages/app/src/components/settings-v2/general-controllers.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, test, vi } from "bun:test" +import { createRoot } from "solid-js" +import { createShellOptions, createSoundPreviewController } from "./general-controller-behavior" + +describe("settings v2 controllers", () => { + test("normalizes shell names and preserves an unavailable configured shell", () => { + expect( + createShellOptions({ + shells: [ + { path: "/bin/bash", name: "bash", acceptable: true }, + { path: "/opt/bash", name: "bash", acceptable: false }, + { path: "/bin/zsh", name: "zsh", acceptable: true }, + ], + current: "fish", + }), + ).toEqual([ + { id: "auto", value: "", name: "", terminalOnly: false }, + { id: "/bin/bash", value: "/bin/bash", name: "/bin/bash", terminalOnly: false }, + { id: "/opt/bash", value: "/opt/bash", name: "/opt/bash", terminalOnly: true }, + { id: "/bin/zsh", value: "zsh", name: "zsh", terminalOnly: false }, + { id: "fish", value: "fish", name: "fish", terminalOnly: false }, + ]) + }) + + test("debounces previews and stops owned audio on disposal", async () => { + vi.useFakeTimers() + try { + const played: string[] = [] + const stopped: string[] = [] + const owned = createRoot((dispose) => ({ + dispose, + preview: createSoundPreviewController(async (id) => { + played.push(id ?? "") + return () => stopped.push(id ?? "") + }), + })) + + owned.preview.play("first") + vi.advanceTimersByTime(99) + expect(played).toEqual([]) + + owned.preview.play("second") + vi.advanceTimersByTime(100) + await Promise.resolve() + expect(played).toEqual(["second"]) + + owned.dispose() + expect(stopped).toEqual(["second"]) + } finally { + vi.useRealTimers() + } + }) +}) diff --git a/packages/app/src/components/settings-v2/general-controllers.ts b/packages/app/src/components/settings-v2/general-controllers.ts new file mode 100644 index 0000000000..ae77fa332f --- /dev/null +++ b/packages/app/src/components/settings-v2/general-controllers.ts @@ -0,0 +1,173 @@ +import { createMemo, createResource, onMount, type Accessor } from "solid-js" +import type { ColorScheme } from "@opencode-ai/ui/theme/context" +import { useTheme } from "@opencode-ai/ui/theme/context" +import { usePermission } from "@/context/permission" +import { useServerSDK } from "@/context/server-sdk" +import { useServerSync } from "@/context/server-sync" +import { + monoDefault, + monoFontFamily, + monoInput, + sansDefault, + sansFontFamily, + sansInput, + terminalDefault, + terminalFontFamily, + terminalInput, + useSettings, +} from "@/context/settings" +import { playSoundById, SOUND_OPTIONS } from "@/utils/sound" +import { createSoundPreviewController, type ShellOption } from "./general-controller-behavior" + +export { createShellOptions, createSoundPreviewController } from "./general-controller-behavior" +export type { ShellOption, ShellSelectOption } from "./general-controller-behavior" + +export function createPermissionScopeController(sessionID: Accessor) { + const permission = usePermission() + const serverSync = useServerSync() + const directory = createMemo(() => { + const id = sessionID() + if (!id) return undefined + return serverSync().session.lineage.peek(id)?.session.directory + }) + + return { + accepting: createMemo(() => { + const id = sessionID() + const dir = directory() + if (!id || !dir) return false + return permission.isAutoAccepting(id, dir) + }), + enabled: createMemo(() => !!directory()), + set: (checked: boolean) => { + const id = sessionID() + const dir = directory() + if (!id || !dir) return + if (checked) return permission.enableAutoAccept(id, dir) + permission.disableAutoAccept(id, dir) + }, + } +} + +export function createShellSettingsController() { + const serverSdk = useServerSDK() + const serverSync = useServerSync() + const [shells] = createResource( + async () => { + const sdk = serverSdk() + if ((await sdk.protocol) === "v1") return (await sdk.client.pty.shells()).data ?? [] + return [] as ShellOption[] + }, + { initialValue: [] as ShellOption[] }, + ) + const current = createMemo(() => serverSync().data.config.shell ?? "") + + return { + shells: () => shells.latest, + current, + select: (value: string) => { + if (value === current()) return + void serverSync().updateConfig({ shell: value }) + }, + } +} + +export function createAppearanceSettingsController() { + const settings = useSettings() + const theme = useTheme() + const themes = createMemo(() => theme.ids().map((id) => ({ id, name: theme.name(id) }))) + + onMount(() => void theme.loadThemes()) + + return { + scheme: { + current: theme.colorScheme, + select: (value: ColorScheme) => theme.setColorScheme(value), + }, + theme: { + options: themes, + current: createMemo(() => themes().find((option) => option.id === theme.themeId())), + select: (option: { id: string } | null) => option && theme.setTheme(option.id), + }, + fonts: { + ui: createMemo(() => ({ + value: sansInput(settings.appearance.uiFont()), + family: sansFontFamily(settings.appearance.uiFont()), + placeholder: sansDefault, + })), + code: createMemo(() => ({ + value: monoInput(settings.appearance.font()), + family: monoFontFamily(settings.appearance.font()), + placeholder: monoDefault, + })), + terminal: createMemo(() => ({ + value: terminalInput(settings.appearance.terminalFont()), + family: terminalFontFamily(settings.appearance.terminalFont()), + placeholder: terminalDefault, + })), + setUI: (value: string) => settings.appearance.setUIFont(value), + setCode: (value: string) => settings.appearance.setFont(value), + setTerminal: (value: string) => settings.appearance.setTerminalFont(value), + }, + } +} + +const noneSound = { id: "none", label: "sound.option.none" } as const +export const soundOptions = [noneSound, ...SOUND_OPTIONS] +export type SoundSelectOption = (typeof soundOptions)[number] + +export function createSoundSettingsController() { + const settings = useSettings() + const preview = createSoundPreviewController(playSoundById) + const channel = ( + enabled: Accessor, + current: Accessor, + setEnabled: (value: boolean) => void, + set: (id: string) => void, + ) => ({ + current: createMemo(() => + enabled() ? (soundOptions.find((option) => option.id === current()) ?? noneSound) : noneSound, + ), + highlight: (option: SoundSelectOption | undefined) => { + if (!option) return + preview.play(option.id === "none" ? undefined : option.id) + }, + select: (option: SoundSelectOption | null) => { + if (!option) return + if (option.id === "none") { + setEnabled(false) + preview.stop() + return + } + setEnabled(true) + set(option.id) + preview.play(option.id) + }, + }) + + return { + agent: channel( + settings.sounds.agentEnabled, + settings.sounds.agent, + (value) => settings.sounds.setAgentEnabled(value), + (id) => settings.sounds.setAgent(id), + ), + permissions: channel( + settings.sounds.permissionsEnabled, + settings.sounds.permissions, + (value) => settings.sounds.setPermissionsEnabled(value), + (id) => settings.sounds.setPermissions(id), + ), + errors: channel( + settings.sounds.errorsEnabled, + settings.sounds.errors, + (value) => settings.sounds.setErrorsEnabled(value), + (id) => settings.sounds.setErrors(id), + ), + } +} + +export type PermissionScopeController = ReturnType +export type ShellSettingsController = ReturnType +export type AppearanceSettingsController = ReturnType +export type SoundSettingsController = ReturnType diff --git a/packages/app/src/components/settings-v2/general.tsx b/packages/app/src/components/settings-v2/general.tsx index 613ac96cba..e50bd316c9 100644 --- a/packages/app/src/components/settings-v2/general.tsx +++ b/packages/app/src/components/settings-v2/general.tsx @@ -1,182 +1,297 @@ -import { Component, Show, createMemo, createResource, onMount } from "solid-js" +import { Component, Show, createMemo, createResource } from "solid-js" import { createMediaQuery } from "@solid-primitives/media" import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" import { SelectV2 } from "@opencode-ai/ui/v2/select-v2" import { Switch } from "@opencode-ai/ui/v2/switch-v2" import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" -import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context" import { useDialog } from "@opencode-ai/ui/context/dialog" import { useLanguage } from "@/context/language" -import { usePermission } from "@/context/permission" import { usePlatform } from "@/context/platform" -import { useServerSync } from "@/context/server-sync" -import { useServerSDK } from "@/context/server-sdk" import { useUpdaterAction } from "../updater-action" -import { - monoDefault, - monoFontFamily, - monoInput, - sansDefault, - sansFontFamily, - sansInput, - terminalDefault, - terminalFontFamily, - terminalInput, - useSettings, -} from "@/context/settings" -import { playSoundById, SOUND_OPTIONS } from "@/utils/sound" +import { useSettings } from "@/context/settings" import { Link } from "../link" import { SettingsListV2 } from "./parts/list" import { SettingsRowV2 } from "./parts/row" import { LayoutRetirementNotice, LayoutTransitionToggle } from "./interface-transition" +import { + createAppearanceSettingsController, + createPermissionScopeController, + createShellOptions, + createShellSettingsController, + createSoundSettingsController, + soundOptions, + type AppearanceSettingsController, + type PermissionScopeController, + type ShellSettingsController, + type SoundSettingsController, +} from "./general-controllers" import "./settings-v2.css" -let demoSoundState = { - cleanup: undefined as (() => void) | undefined, - timeout: undefined as NodeJS.Timeout | undefined, - run: 0, +const schemeOptions: ("system" | "light" | "dark")[] = ["system", "light", "dark"] +const fontSettings = { + ui: { + action: "settings-ui-font", + title: "settings.general.row.uiFont.title", + description: "settings.general.row.uiFont.description", + font: "ui", + input: "setUI", + }, + code: { + action: "settings-code-font", + title: "settings.general.row.font.title", + description: "settings.general.row.font.description", + font: "code", + input: "setCode", + }, + terminal: { + action: "settings-terminal-font", + title: "settings.general.row.terminalFont.title", + description: "settings.general.row.terminalFont.description", + font: "terminal", + input: "setTerminal", + }, +} as const +const soundSettings = { + agent: { + action: "settings-sounds-agent", + title: "settings.general.sounds.agent.title", + description: "settings.general.sounds.agent.description", + }, + permissions: { + action: "settings-sounds-permissions", + title: "settings.general.sounds.permissions.title", + description: "settings.general.sounds.permissions.description", + }, + errors: { + action: "settings-sounds-errors", + title: "settings.general.sounds.errors.title", + description: "settings.general.sounds.errors.description", + }, +} as const + +const PermissionScopeSetting: Component<{ controller: PermissionScopeController }> = (props) => { + const language = useLanguage() + return ( + +
+ +
+
+ ) } -type ThemeOption = { - id: string - name: string +const ShellSetting: Component<{ controller: ShellSettingsController }> = (props) => { + const language = useLanguage() + const options = createMemo(() => + createShellOptions({ + shells: props.controller.shells(), + current: props.controller.current(), + }), + ) + return ( + + option.value === props.controller.current()) ?? options()[0]} + placement="bottom-end" + gutter={6} + value={(option) => option.id} + label={(option) => { + if (option.id === "auto") return language.t("settings.general.row.shell.autoDefault") + if (!option.terminalOnly) return option.name + return `${option.name} (${language.t("settings.general.row.shell.terminalOnly")})` + }} + onSelect={(option) => option && props.controller.select(option.value)} + /> + + ) } -type ShellOption = { - path: string - name: string - acceptable: boolean +const AppearanceSection: Component<{ controller: AppearanceSettingsController }> = (props) => { + const language = useLanguage() + return ( +
+

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

+ + + option === props.controller.scheme.current())} + placement="bottom-end" + gutter={6} + label={(option) => { + if (option === "system") return language.t("theme.scheme.system") + if (option === "light") return language.t("theme.scheme.light") + return language.t("theme.scheme.dark") + }} + onSelect={(option) => option && props.controller.scheme.select(option)} + /> + + + + {language.t("settings.general.row.theme.description")}{" "} + + {language.t("common.learnMore")} + + + } + > + option.id} + label={(option) => option.name} + onSelect={props.controller.theme.select} + /> + + + + + + +
+ ) } -type ShellSelectOption = { - id: string - value: string - label: string +const FontSetting: Component<{ + kind: "ui" | "code" | "terminal" + fonts: AppearanceSettingsController["fonts"] +}> = (props) => { + const language = useLanguage() + const config = () => fontSettings[props.kind] + return ( + +
+ props.fonts[config().input](event.currentTarget.value)} + placeholder={props.fonts[config().font]().placeholder} + spellcheck={false} + autocorrect="off" + autocomplete="off" + autocapitalize="off" + aria-label={language.t(config().title)} + style={{ "font-family": props.fonts[config().font]().family }} + /> +
+
+ ) } -// To prevent audio from overlapping/playing very quickly when navigating the settings menus, -// delay the playback by 100ms during quick selection changes and pause existing sounds. -const stopDemoSound = () => { - demoSoundState.run += 1 - if (demoSoundState.cleanup) { - demoSoundState.cleanup() - } - clearTimeout(demoSoundState.timeout) - demoSoundState.cleanup = undefined +const SoundsSection: Component<{ controller: SoundSettingsController }> = (props) => { + const language = useLanguage() + return ( +
+

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

+ + + + + +
+ ) } -const playDemoSound = (id: string | undefined) => { - stopDemoSound() - if (!id) return +const SoundSetting: Component<{ + kind: "agent" | "permissions" | "errors" + channel: SoundSettingsController["agent"] +}> = (props) => { + const language = useLanguage() + const config = () => soundSettings[props.kind] + return ( + + option.id} + label={(option) => language.t(option.label)} + onHighlight={props.channel.highlight} + onSelect={props.channel.select} + placement="bottom-end" + gutter={6} + /> + + ) +} - const run = ++demoSoundState.run - demoSoundState.timeout = setTimeout(() => { - void playSoundById(id).then((cleanup) => { - if (demoSoundState.run !== run) { - cleanup?.() - return - } - demoSoundState.cleanup = cleanup - }) - }, 100) +const LanguageSetting = () => { + const language = useLanguage() + const options = createMemo(() => + language.locales.map((locale) => ({ + value: locale, + label: language.label(locale), + })), + ) + return ( + + option.value === language.locale())} + value={(option) => option.value} + label={(option) => option.label} + onSelect={(option) => option && language.setLocale(option.value)} + /> + + ) } export const SettingsGeneralV2: Component<{ sessionID?: string }> = (props) => { - const theme = useTheme() const language = useLanguage() - const permission = usePermission() const platform = usePlatform() const dialog = useDialog() const settings = useSettings() - const serverSync = useServerSync() - const serverSdk = useServerSDK() const mobile = createMediaQuery("(max-width: 767px)") - const updater = useUpdaterAction() - - const dir = createMemo(() => { - if (!props.sessionID) return undefined - return serverSync().session.lineage.peek(props.sessionID)?.session.directory - }) - const accepting = createMemo(() => { - const value = dir() - if (!value || !props.sessionID) return false - return permission.isAutoAccepting(props.sessionID, value) - }) - - const toggleAccept = (checked: boolean) => { - const value = dir() - if (!value || !props.sessionID) return - - if (checked) { - permission.enableAutoAccept(props.sessionID, value) - return - } - - permission.disableAutoAccept(props.sessionID, value) - } + const permissionScope = createPermissionScopeController(() => props.sessionID) + const shell = createShellSettingsController() + const appearance = createAppearanceSettingsController() + const sounds = createSoundSettingsController() const desktop = createMemo(() => platform.platform === "desktop") - const themeOptions = createMemo(() => theme.ids().map((id) => ({ id, name: theme.name(id) }))) - - const [shells] = createResource( - async () => { - const sdk = serverSdk() - if ((await sdk.protocol) === "v1") { - return (await sdk.client.pty.shells()).data ?? [] - } - // return (await sdk.api.pty.shells()).data - return [] as ShellOption[] - }, - { initialValue: [] as ShellOption[] }, - ) - const [pinchZoom, { mutate: setPinchZoom }] = createResource( - () => (desktop() && platform.getPinchZoomEnabled ? true : false), + () => desktop() && "getPinchZoomEnabled" in platform, () => Promise.resolve(platform.getPinchZoomEnabled?.() ?? false).catch(() => false), { initialValue: false }, ) - onMount(() => { - void theme.loadThemes() - }) - - const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") } - const currentShell = createMemo(() => serverSync().data.config.shell ?? "") - - const shellOptions = createMemo(() => { - const list = shells.latest - const current = serverSync().data.config.shell - - const nameCounts = new Map() - for (const s of list) { - nameCounts.set(s.name, (nameCounts.get(s.name) || 0) + 1) - } - - const options = [ - autoOption, - ...list.map((s) => { - const ambiguousName = (nameCounts.get(s.name) || 0) > 1 - const text = ambiguousName ? s.path : s.name - const label = s.acceptable ? text : `${text} (${language.t("settings.general.row.shell.terminalOnly")})` - return { - id: s.path, - // Prefer name over path - "bash" is much cleaner than the explicit full route even when it may change due to PATH. - value: ambiguousName ? s.path : s.name, - label, - } - }), - ] - - if (current && !options.some((o) => o.value === current)) { - options.push({ id: current, value: current, label: current }) - } - - return options - }) - const onPinchZoomChange = (checked: boolean) => { setPinchZoom(checked) const update = platform.setPinchZoomEnabled?.(checked) @@ -184,52 +299,6 @@ export const SettingsGeneralV2: Component<{ void update.catch(() => setPinchZoom(!checked)) } - const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [ - { value: "system", label: language.t("theme.scheme.system") }, - { value: "light", label: language.t("theme.scheme.light") }, - { value: "dark", label: language.t("theme.scheme.dark") }, - ]) - - const languageOptions = createMemo(() => - language.locales.map((locale) => ({ - value: locale, - label: language.label(locale), - })), - ) - - const noneSound = { id: "none", label: "sound.option.none" } as const - 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, - current: () => string, - setEnabled: (value: boolean) => void, - set: (id: string) => void, - ) => ({ - options: soundOptions, - current: enabled() ? (soundOptions.find((o) => o.id === current()) ?? noneSound) : noneSound, - value: (o: (typeof soundOptions)[number]) => o.id, - label: (o: (typeof soundOptions)[number]) => language.t(o.label), - onHighlight: (option: (typeof soundOptions)[number] | undefined) => { - if (!option) return - playDemoSound(option.id === "none" ? undefined : option.id) - }, - onSelect: (option: (typeof soundOptions)[number] | null) => { - if (!option) return - if (option.id === "none") { - setEnabled(false) - stopDemoSound() - return - } - setEnabled(true) - set(option.id) - playDemoSound(option.id) - }, - }) - const InterfaceSection = () => ( settings.general.dismissNewInterfaceNotice()} /> ) const GeneralSection = () => (
- - o.value === language.locale())} - value={(o) => o.value} - label={(o) => o.label} - onSelect={(option) => option && language.setLocale(option.value)} - /> - + - -
- -
-
+ - - o.value === currentShell()) ?? autoOption} - placement="bottom-end" - gutter={6} - value={(o) => o.id} - label={(o) => o.label} - onSelect={(option) => { - if (!option) return - if (option.value === currentShell()) return - serverSync().updateConfig({ shell: option.value }) - }} - /> - + ) - const AppearanceSection = () => ( -
-

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

- - - - o.value === theme.colorScheme())} - placement="bottom-end" - gutter={6} - value={(o) => o.value} - label={(o) => o.label} - onSelect={(option) => option && theme.setColorScheme(option.value)} - /> - - - - {language.t("settings.general.row.theme.description")}{" "} - - {language.t("common.learnMore")} - - - } - > - o.id === theme.themeId())} - placement="bottom-end" - gutter={6} - value={(o) => o.id} - label={(o) => o.name} - onSelect={(option) => { - if (!option) return - theme.setTheme(option.id) - }} - /> - - - -
- settings.appearance.setUIFont(event.currentTarget.value)} - placeholder={sansDefault} - spellcheck={false} - autocorrect="off" - autocomplete="off" - autocapitalize="off" - aria-label={language.t("settings.general.row.uiFont.title")} - style={{ "font-family": sansFontFamily(settings.appearance.uiFont()) }} - /> -
-
- - -
- settings.appearance.setFont(event.currentTarget.value)} - placeholder={monoDefault} - spellcheck={false} - autocorrect="off" - autocomplete="off" - autocapitalize="off" - aria-label={language.t("settings.general.row.font.title")} - style={{ "font-family": monoFontFamily(settings.appearance.font()) }} - /> -
-
- - -
- settings.appearance.setTerminalFont(event.currentTarget.value)} - placeholder={terminalDefault} - spellcheck={false} - autocorrect="off" - autocomplete="off" - autocapitalize="off" - aria-label={language.t("settings.general.row.terminalFont.title")} - style={{ "font-family": terminalFontFamily(settings.appearance.terminalFont()) }} - /> -
-
-
-
- ) - const NotificationsSection = () => (

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

@@ -576,68 +486,6 @@ export const SettingsGeneralV2: Component<{
) - const SoundsSection = () => ( -
-

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

- - - - settings.sounds.agentEnabled(), - () => settings.sounds.agent(), - (value) => settings.sounds.setAgentEnabled(value), - (id) => settings.sounds.setAgent(id), - )} - placement="bottom-end" - gutter={6} - /> - - - - settings.sounds.permissionsEnabled(), - () => settings.sounds.permissions(), - (value) => settings.sounds.setPermissionsEnabled(value), - (id) => settings.sounds.setPermissions(id), - )} - placement="bottom-end" - gutter={6} - /> - - - - settings.sounds.errorsEnabled(), - () => settings.sounds.errors(), - (value) => settings.sounds.setErrorsEnabled(value), - (id) => settings.sounds.setErrors(id), - )} - placement="bottom-end" - gutter={6} - /> - - -
- ) - const UpdatesSection = () => (

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

@@ -659,7 +507,7 @@ export const SettingsGeneralV2: Component<{ title={language.t("settings.updates.row.check.title")} description={language.t("settings.updates.row.check.description")} > - + updater.run()}> {language.t(updater.action().label)} @@ -704,11 +552,11 @@ export const SettingsGeneralV2: Component<{ - + - + From 1ead8d84a2f98caaf527a66e86e7606292210780 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:28:59 +0800 Subject: [PATCH 058/129] fix(app): localize home session suspense (#39285) --- .../app/src/pages/home/home-sessions-view.tsx | 38 +++++++++---------- packages/app/src/pages/home/home-sessions.tsx | 1 - 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/app/src/pages/home/home-sessions-view.tsx b/packages/app/src/pages/home/home-sessions-view.tsx index 461322ae43..72f2f37616 100644 --- a/packages/app/src/pages/home/home-sessions-view.tsx +++ b/packages/app/src/pages/home/home-sessions-view.tsx @@ -1,5 +1,5 @@ import type { Session } from "@opencode-ai/sdk/v2/client" -import { type Accessor, createMemo, For, Show } from "solid-js" +import { type Accessor, createMemo, For, Show, Suspense } from "solid-js" import { Spinner } from "@opencode-ai/ui/spinner" import { ScrollView } from "@opencode-ai/ui/scroll-view" import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" @@ -39,7 +39,6 @@ function isBackgroundOpen(event: MouseEvent) { export type HomeSessionsViewProps = { language: ReturnType groups: Accessor - loading: Accessor showProjectName: Accessor server: Accessor canCreateSession: Accessor @@ -81,20 +80,22 @@ export function HomeSessionsView(props: HomeSessionsViewProps) { >
- 0 && props.canCreateSession()}> -
- - {props.language.t("command.session.new")} - -
-
+ + 0 && props.canCreateSession()}> +
+ + {props.language.t("command.session.new")} + +
+
+
) diff --git a/packages/app/src/pages/home/home-sessions.tsx b/packages/app/src/pages/home/home-sessions.tsx index 2e3828fd8c..7bda4dd363 100644 --- a/packages/app/src/pages/home/home-sessions.tsx +++ b/packages/app/src/pages/home/home-sessions.tsx @@ -12,7 +12,6 @@ export function HomeSessions(props: { Date: Tue, 28 Jul 2026 17:29:27 +0800 Subject: [PATCH 059/129] feat(desktop): add opt-in v2 sidecar (#39286) --- .github/workflows/publish.yml | 4 - .../desktop/electron-builder.config.test.ts | 33 +++++ packages/desktop/electron-builder.config.ts | 11 +- packages/desktop/scripts/prebuild.ts | 3 +- packages/desktop/scripts/predev.ts | 2 + packages/desktop/scripts/utils.ts | 64 ++++++--- packages/desktop/src/main/background-cli.ts | 125 ++++++++++++++++++ packages/desktop/src/main/index.ts | 77 +++++++---- packages/desktop/src/main/server.ts | 4 +- 9 files changed, 265 insertions(+), 58 deletions(-) create mode 100644 packages/desktop/src/main/background-cli.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 037020c03a..15f865b742 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -219,7 +219,6 @@ jobs: build-electron: needs: - - build-cli - version if: github.repository == 'anomalyco/opencode' continue-on-error: false @@ -316,10 +315,7 @@ jobs: env: OPENCODE_VERSION: ${{ needs.version.outputs.version }} OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} - OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }} RUST_TARGET: ${{ matrix.settings.target }} - GH_TOKEN: ${{ github.token }} - GITHUB_RUN_ID: ${{ github.run_id }} - name: Build run: bun run build diff --git a/packages/desktop/electron-builder.config.test.ts b/packages/desktop/electron-builder.config.test.ts index 4ae53fa043..3fb1adb6c1 100644 --- a/packages/desktop/electron-builder.config.test.ts +++ b/packages/desktop/electron-builder.config.test.ts @@ -56,3 +56,36 @@ test("keeps a hidden prod launcher for old Linux pins", async () => { expect(desktop).toContain("StartupWMClass=ai.opencode.desktop") expect(desktop).toContain("NoDisplay=true") }) + +test("bundles the CLI outside the dev app archive", async () => { + const previous = process.env.OPENCODE_CHANNEL + process.env.OPENCODE_CHANNEL = "dev" + const module = await import("./electron-builder.config.ts?cli-resource") + const config = module.default as Configuration + if (previous === undefined) delete process.env.OPENCODE_CHANNEL + else process.env.OPENCODE_CHANNEL = previous + + expect(config.files).toContain("!resources/opencode-cli*") + expect(config.extraResources).toContainEqual({ + from: "resources/", + to: "", + filter: ["opencode-cli*"], + }) +}) + +for (const channel of ["beta", "prod"] as const) { + test(`does not bundle the CLI in ${channel} builds`, async () => { + const previous = process.env.OPENCODE_CHANNEL + process.env.OPENCODE_CHANNEL = channel + const module = await import(`./electron-builder.config.ts?no-cli-resource=${channel}`) + const config = module.default as Configuration + if (previous === undefined) delete process.env.OPENCODE_CHANNEL + else process.env.OPENCODE_CHANNEL = previous + + expect(config.extraResources).not.toContainEqual({ + from: "resources/", + to: "", + filter: ["opencode-cli*"], + }) + }) +} diff --git a/packages/desktop/electron-builder.config.ts b/packages/desktop/electron-builder.config.ts index a10a757dbb..508c0df5e9 100644 --- a/packages/desktop/electron-builder.config.ts +++ b/packages/desktop/electron-builder.config.ts @@ -55,8 +55,17 @@ const getBase = (appId: string): Configuration => ({ extraMetadata: { desktopName: `${appId}.desktop`, }, - files: ["out/**/*", "resources/**/*"], + files: ["out/**/*", "resources/**/*", "!resources/opencode-cli*"], extraResources: [ + ...(channel === "dev" + ? [ + { + from: "resources/", + to: "", + filter: ["opencode-cli*"], + }, + ] + : []), { from: "native/", to: "native/", diff --git a/packages/desktop/scripts/prebuild.ts b/packages/desktop/scripts/prebuild.ts index 79b0e30afc..636850a387 100644 --- a/packages/desktop/scripts/prebuild.ts +++ b/packages/desktop/scripts/prebuild.ts @@ -1,10 +1,11 @@ #!/usr/bin/env bun import { $ } from "bun" -import { resolveChannel } from "./utils" +import { downloadCliToResources, resolveChannel } from "./utils" const channel = resolveChannel() await $`bun ./scripts/copy-icons.ts ${channel}` await $`bun ./scripts/copy-metainfo.ts ${channel}` await $`cd ../opencode && bun script/build-node.ts` +if (channel === "dev") await downloadCliToResources() diff --git a/packages/desktop/scripts/predev.ts b/packages/desktop/scripts/predev.ts index 4a9682306d..bfa399e4e5 100644 --- a/packages/desktop/scripts/predev.ts +++ b/packages/desktop/scripts/predev.ts @@ -1,7 +1,9 @@ import { $ } from "bun" +import { downloadCliToResources } from "./utils" await $`bun run install-electron` await $`bun ./scripts/copy-icons.ts ${process.env.OPENCODE_CHANNEL ?? "dev"}` await $`cd ../opencode && bun script/build-node.ts` +await downloadCliToResources() diff --git a/packages/desktop/scripts/utils.ts b/packages/desktop/scripts/utils.ts index 19b96b0a16..bf728758ed 100644 --- a/packages/desktop/scripts/utils.ts +++ b/packages/desktop/scripts/utils.ts @@ -1,4 +1,9 @@ import { $ } from "bun" +import { chmod, copyFile, mkdtemp, rm } from "node:fs/promises" +import { tmpdir } from "node:os" +import { join } from "node:path" + +const CLI_VERSION = "0.0.0-next-16350" export type Channel = "dev" | "beta" | "prod" @@ -8,36 +13,42 @@ export function resolveChannel(): Channel { return "dev" } -export const SIDECAR_BINARIES: Array<{ rustTarget: string; ocBinary: string; assetExt: string }> = [ +export const CLI_BINARIES: Array<{ rustTarget: string; package: string; os: string; cpu: string }> = [ { rustTarget: "aarch64-apple-darwin", - ocBinary: "opencode-darwin-arm64", - assetExt: "zip", + package: "@opencode-ai/cli-darwin-arm64", + os: "darwin", + cpu: "arm64", }, { rustTarget: "x86_64-apple-darwin", - ocBinary: "opencode-darwin-x64-baseline", - assetExt: "zip", + package: "@opencode-ai/cli-darwin-x64-baseline", + os: "darwin", + cpu: "x64", }, { rustTarget: "aarch64-pc-windows-msvc", - ocBinary: "opencode-windows-arm64", - assetExt: "zip", + package: "@opencode-ai/cli-windows-arm64", + os: "win32", + cpu: "arm64", }, { rustTarget: "x86_64-pc-windows-msvc", - ocBinary: "opencode-windows-x64-baseline", - assetExt: "zip", + package: "@opencode-ai/cli-windows-x64-baseline", + os: "win32", + cpu: "x64", }, { rustTarget: "x86_64-unknown-linux-gnu", - ocBinary: "opencode-linux-x64-baseline", - assetExt: "tar.gz", + package: "@opencode-ai/cli-linux-x64-baseline", + os: "linux", + cpu: "x64", }, { rustTarget: "aarch64-unknown-linux-gnu", - ocBinary: "opencode-linux-arm64", - assetExt: "tar.gz", + package: "@opencode-ai/cli-linux-arm64", + os: "linux", + cpu: "arm64", }, ] @@ -51,24 +62,33 @@ function nativeTarget() { throw new Error(`Unsupported platform: ${platform}/${arch}`) } -export function getCurrentSidecar(target = RUST_TARGET ?? nativeTarget()) { - const binaryConfig = SIDECAR_BINARIES.find((b) => b.rustTarget === target) - if (!binaryConfig) throw new Error(`Sidecar configuration not available for Rust target '${target}'`) +export function getCurrentCli(target = RUST_TARGET ?? nativeTarget()) { + const binaryConfig = CLI_BINARIES.find((item) => item.rustTarget === target) + if (!binaryConfig) throw new Error(`CLI configuration not available for target '${target}'`) return binaryConfig } -export async function copyBinaryToSidecarFolder(source: string) { - const dir = `resources` - await $`mkdir -p ${dir}` - const dest = windowsify(`${dir}/opencode-cli`) - await $`cp ${source} ${dest}` +export async function downloadCliToResources() { + const cli = getCurrentCli() + const directory = await mkdtemp(join(tmpdir(), "opencode-cli-")) + const dest = windowsify("resources/opencode-cli") + try { + await $`bun install --no-save --cwd ${directory} ${`${cli.package}@${CLI_VERSION}`} ${`--os=${cli.os}`} ${`--cpu=${cli.cpu}`}` + await copyFile( + join(directory, "node_modules", cli.package, "bin", cli.os === "win32" ? "opencode2.exe" : "opencode2"), + dest, + ) + } finally { + await rm(directory, { recursive: true, force: true }) + } + if (process.platform !== "win32") await chmod(dest, 0o755) if (process.platform === "win32" && process.env.GITHUB_ACTIONS === "true") { await $`pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File ../../script/sign-windows.ps1 ${dest}` } if (process.platform === "darwin") await $`codesign --force --sign - ${dest}` - console.log(`Copied ${source} to ${dest}`) + console.log(`Copied ${cli.package} to ${dest}`) } export function windowsify(path: string) { diff --git a/packages/desktop/src/main/background-cli.ts b/packages/desktop/src/main/background-cli.ts new file mode 100644 index 0000000000..66602d51f1 --- /dev/null +++ b/packages/desktop/src/main/background-cli.ts @@ -0,0 +1,125 @@ +import { execFile } from "node:child_process" +import { existsSync } from "node:fs" +import { chmod, copyFile, mkdir, rename, rm } from "node:fs/promises" +import { dirname, join } from "node:path" +import { fileURLToPath } from "node:url" +import { promisify } from "node:util" +import { app } from "electron" + +const execFileAsync = promisify(execFile) +const root = dirname(fileURLToPath(import.meta.url)) +const stateHome = process.env.XDG_STATE_HOME +const desktopStateNames = ["ai.opencode.desktop.dev", "ai.opencode.desktop.beta", "ai.opencode.desktop"] + +type Logger = { + log(message: string, meta?: Record): void + error(message: string, meta?: Record): void +} + +export async function startBackgroundCli(logger: Logger, shellStateHome?: string) { + const bundled = app.isPackaged + ? join(process.resourcesPath, executableName()) + : join(root, "../../resources", executableName()) + logger.log("v2 CLI executable resolved", { bundled, packaged: app.isPackaged }) + const version = await run(bundled, ["--version"], logger) + const binary = app.isPackaged ? await installCli(bundled, version, logger) : bundled + + const candidates = [ + ...new Set([stateHome, shellStateHome, ...desktopStateNames.map((name) => join(app.getPath("appData"), name))]), + ].filter((candidate) => candidate === undefined || existsSync(candidate)) + const discovered = await Promise.all( + candidates.map(async (candidate) => ({ + stateHome: candidate, + url: serviceUrl(await run(binary, ["service", "status"], logger, { stateHome: candidate })), + })), + ) + const found = discovered.find((candidate) => candidate.url !== undefined) + logger.log("v2 CLI background instance checked", { + detected: Boolean(found), + ...endpoint(found?.url), + }) + + const daemonStateHome = found?.stateHome ?? stateHome + const url = await run(binary, ["service", "start"], logger, { stateHome: daemonStateHome }) + const password = await run(binary, ["service", "get", "password"], logger, { + redact: true, + stateHome: daemonStateHome, + }) + logger.log("v2 CLI background service ready", { + existing: Boolean(found), + username: "opencode", + ...endpoint(url), + }) + return { + url, + username: "opencode", + password, + } +} + +async function installCli(source: string, version: string, logger: Logger) { + const directory = join(app.getPath("userData"), "cli", version.replace(/[^a-zA-Z0-9._-]/g, "-")) + const destination = join(directory, executableName()) + if (existsSync(destination)) { + logger.log("v2 CLI staged executable reused", { path: destination, version }) + return destination + } + + const temp = destination + `.${process.pid}.tmp` + await mkdir(directory, { recursive: true }) + await copyFile(source, temp) + if (process.platform !== "win32") await chmod(temp, 0o755) + await rename(temp, destination).catch(async (error) => { + await rm(temp, { force: true }) + throw error + }) + logger.log("v2 CLI executable staged", { source, path: destination, version }) + return destination +} + +async function run( + binary: string, + args: string[], + logger: Logger, + options: { redact?: boolean; stateHome?: string } = {}, +) { + logger.log("v2 CLI command started", { binary, args }) + const env = { ...process.env } + if (options.stateHome === undefined) delete env.XDG_STATE_HOME + else env.XDG_STATE_HOME = options.stateHome + return execFileAsync(binary, args, { env, windowsHide: true }).then( + (result) => { + const stdout = result.stdout.trim() + const stderr = result.stderr.trim() + logger.log("v2 CLI command completed", { args, stdout: options.redact ? "[redacted]" : stdout, stderr }) + return stdout + }, + (error: unknown) => { + const output = error as { stdout?: string; stderr?: string } + logger.error("v2 CLI command failed", { + args, + error: error instanceof Error ? error.message : String(error), + stdout: options.redact && output.stdout ? "[redacted]" : (output.stdout?.trim() ?? ""), + stderr: output.stderr?.trim() ?? "", + }) + throw error + }, + ) +} + +function serviceUrl(status: string) { + if (URL.canParse(status)) return status + if (!status.startsWith("running ")) return + const url = status.slice("running ".length).trim() + return URL.canParse(url) ? url : undefined +} + +function endpoint(url: string | undefined) { + if (!url || !URL.canParse(url)) return {} + const parsed = new URL(url) + return { url, hostname: parsed.hostname, port: parsed.port } +} + +function executableName() { + return process.platform === "win32" ? "opencode-cli.exe" : "opencode-cli" +} diff --git a/packages/desktop/src/main/index.ts b/packages/desktop/src/main/index.ts index c7c1643092..67696a5470 100644 --- a/packages/desktop/src/main/index.ts +++ b/packages/desktop/src/main/index.ts @@ -48,6 +48,7 @@ import { registerWslIpcHandlers } from "./wsl/ipc" import { spawnWslSidecar } from "./wsl/sidecar" import { migrate } from "./migrate" import { cleanupStoreFiles } from "./store-cleanup" +import { startBackgroundCli } from "./background-cli" const APP_NAMES: Record = { dev: "OpenCode Dev", @@ -60,6 +61,7 @@ const APP_IDS: Record = { prod: "ai.opencode.desktop", } const TEST_ONBOARDING = process.env.OPENCODE_TEST_ONBOARDING === "1" +const SIDECAR_VERSION = process.env.OPENCODE_SIDECAR_V2 === "1" ? "v2" : "v1" const jsCallStackFeature = "DocumentPolicyIncludeJSCallStacksInCrashReports" let logger: ReturnType @@ -198,7 +200,7 @@ const main = Effect.gen(function* () { return } - preferAppEnv(app.getPath("userData")) + const shellEnv = preferAppEnv(app.getPath("userData")) app.on("second-instance", (_event: Event, argv: string[]) => { const urls = argv.filter((arg: string) => arg.startsWith("opencode://")) @@ -312,39 +314,56 @@ const main = Effect.gen(function* () { ), ) - const port = yield* Effect.gen(function* () { - const fromEnv = process.env.OPENCODE_PORT - if (fromEnv) { - const parsed = Number.parseInt(fromEnv, 10) - if (!Number.isNaN(parsed)) return parsed - } - - const res = yield* Deferred.make() - const server = createServer() - server.on("error", (e) => Deferred.failSync(res, () => e)) - server.listen(0, "127.0.0.1", () => { - const address = server.address() - if (typeof address !== "object" || !address) { - server.close() - Deferred.failSync(res, () => new Error("Failed to get port")) - return - } - const port = address.port - server.close(() => Effect.runSync(Deferred.succeed(res, port))) - }) - - return yield* Deferred.await(res) - }) - const hostname = "127.0.0.1" - const url = `http://${hostname}:${port}` - const password = randomUUID() - const loadingTask = yield* Effect.gen(function* () { - logger.log("sidecar connection started", { url }) + logger.log("sidecar connection started", { version: SIDECAR_VERSION }) ensureLoopbackNoProxy() useEnvProxy() + if (SIDECAR_VERSION === "v2") { + logger.log("spawning v2 sidecar") + const sidecar = yield* Effect.promise(() => startBackgroundCli(logger, shellEnv?.XDG_STATE_HOME)) + yield* Deferred.succeed(serverReady, { + url: sidecar.url, + username: sidecar.username, + password: sidecar.password, + }) + + if (process.platform === "win32") { + void wslServers.initialize().catch((error) => logger.error("wsl server initialization failed", error)) + } + + logger.log("loading task finished") + return + } + + const port = yield* Effect.gen(function* () { + const fromEnv = process.env.OPENCODE_PORT + if (fromEnv) { + const parsed = Number.parseInt(fromEnv, 10) + if (!Number.isNaN(parsed)) return parsed + } + + const res = yield* Deferred.make() + const socket = createServer() + socket.on("error", (e) => Deferred.failSync(res, () => e)) + socket.listen(0, "127.0.0.1", () => { + const address = socket.address() + if (typeof address !== "object" || !address) { + socket.close() + Deferred.failSync(res, () => new Error("Failed to get port")) + return + } + const port = address.port + socket.close(() => Effect.runSync(Deferred.succeed(res, port))) + }) + + return yield* Deferred.await(res) + }) + const hostname = "127.0.0.1" + const url = `http://${hostname}:${port}` + const password = randomUUID() + logger.log("spawning sidecar", { url }) const { listener, health } = yield* Effect.promise(() => spawnLocalServer(hostname, port, password, { diff --git a/packages/desktop/src/main/server.ts b/packages/desktop/src/main/server.ts index c620b76ae8..ae1a98efdf 100644 --- a/packages/desktop/src/main/server.ts +++ b/packages/desktop/src/main/server.ts @@ -43,13 +43,15 @@ export function setDefaultServerUrl(url: string | null) { export function preferAppEnv(userDataPath: string) { const shell = process.platform === "win32" ? null : getUserShell() + const shellEnv = shell ? loadShellEnv(shell, getLogger()) : null Object.assign(process.env, { - ...(shell ? loadShellEnv(shell, getLogger()) : null), + ...shellEnv, OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true", OPENCODE_EXPERIMENTAL_FILEWATCHER: "true", OPENCODE_CLIENT: "desktop", XDG_STATE_HOME: process.env.XDG_STATE_HOME ?? userDataPath, }) + return shellEnv } export async function spawnLocalServer( From 7c898249a23cce3ebe3ce49008b03b5e14d9f4ad Mon Sep 17 00:00:00 2001 From: Robin Andrew Date: Tue, 28 Jul 2026 12:05:57 +0100 Subject: [PATCH 060/129] fix(desktop): patch @dnd-kit/solid to preserve core scroll plugins (#38119) Co-authored-by: Brendan Allan Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> --- package.json | 1 + patches/@dnd-kit%2Fdom@0.5.0.patch | 40 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 patches/@dnd-kit%2Fdom@0.5.0.patch diff --git a/package.json b/package.json index 662f9a4c3c..e7e081a4d3 100644 --- a/package.json +++ b/package.json @@ -143,6 +143,7 @@ "@types/node": "catalog:" }, "patchedDependencies": { + "@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch", "@ff-labs/fff-bun@0.9.3": "patches/@ff-labs%2Ffff-bun@0.9.3.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", diff --git a/patches/@dnd-kit%2Fdom@0.5.0.patch b/patches/@dnd-kit%2Fdom@0.5.0.patch new file mode 100644 index 0000000000..7c56793f9a --- /dev/null +++ b/patches/@dnd-kit%2Fdom@0.5.0.patch @@ -0,0 +1,40 @@ +--- a/index.js ++++ b/index.js +@@ -2195,6 +2195,17 @@ + sensors, + modifiers + })); ++ } ++ get plugins() { ++ return super.plugins; ++ } ++ set plugins(plugins) { ++ super.plugins = [ ++ ScrollListener, ++ Scroller, ++ StyleInjector, ++ ...plugins ++ ]; + } + }; + var _element_dec, _handle_dec, _c, _init5, _handle, _element; +--- a/index.cjs ++++ b/index.cjs +@@ -2196,6 +2196,17 @@ + sensors, + modifiers + })); ++ } ++ get plugins() { ++ return super.plugins; ++ } ++ set plugins(plugins) { ++ super.plugins = [ ++ ScrollListener, ++ Scroller, ++ StyleInjector, ++ ...plugins ++ ]; + } + }; + var _element_dec, _handle_dec, _c, _init5, _handle, _element; From be93cec9bae8e4bdaaca3bd5cc015f011d71782b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 19:07:19 +0800 Subject: [PATCH 061/129] fix(app): guard reentrant Solid cleanup (#39261) Co-authored-by: Jack --- .../test-browser/solid-router-cleanup.test.ts | 64 +++++++ patches/solid-js@1.9.10.patch | 156 ++++++++++++++++-- 2 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 packages/app/test-browser/solid-router-cleanup.test.ts diff --git a/packages/app/test-browser/solid-router-cleanup.test.ts b/packages/app/test-browser/solid-router-cleanup.test.ts new file mode 100644 index 0000000000..5795fa5648 --- /dev/null +++ b/packages/app/test-browser/solid-router-cleanup.test.ts @@ -0,0 +1,64 @@ +import { expect, test } from "bun:test" +import { MetaProvider, Title } from "@solidjs/meta" +import { MemoryRouter, Route, createMemoryHistory, useParams } from "@solidjs/router" +import { createMemo } from "solid-js" +import { createComponent, render } from "solid-js/web" + +test("route cleanup cannot invalidate an owner list being disposed", async () => { + const host = document.createElement("div") + document.body.append(host) + const history = createMemoryHistory() + + const RepoPage = () => { + const params = useParams<{ id?: string }>() + const title = createMemo(() => params.id ?? "") + const button = document.createElement("button") + button.textContent = "Back" + button.addEventListener("click", () => history.set({ value: "/", scroll: false, replace: false })) + return [ + createComponent(Title, { + get children() { + return title() + }, + }), + button, + ] + } + + const HomePage = () => { + const button = document.createElement("button") + button.textContent = "Go" + button.addEventListener("click", () => history.set({ value: "/project", scroll: false, replace: false })) + return button + } + + const App = () => + createComponent(MetaProvider, { + get children() { + return createComponent(MemoryRouter, { + history, + get children() { + return [ + createComponent(Route, { path: "/", component: HomePage }), + createComponent(Route, { path: "/:id", component: RepoPage }), + ] + }, + }) + }, + }) + + const dispose = render(() => createComponent(App, {}), host) + const go = host.querySelector("button") + expect(go?.textContent).toBe("Go") + go?.click() + await new Promise((resolve) => setTimeout(resolve, 0)) + + const back = host.querySelector("button") + expect(back?.textContent).toBe("Back") + back?.click() + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect(host.querySelector("button")?.textContent).toBe("Go") + dispose() + host.remove() +}) diff --git a/patches/solid-js@1.9.10.patch b/patches/solid-js@1.9.10.patch index e4e38c2e68..0571d5c8c6 100644 --- a/patches/solid-js@1.9.10.patch +++ b/patches/solid-js@1.9.10.patch @@ -1,11 +1,5 @@ -diff --git a/Users/brendonovich/github.com/anomalyco/opencode/node_modules/solid-js/.bun-tag-6fcb6b48d6947d2c b/.bun-tag-6fcb6b48d6947d2c -new file mode 100644 -index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 -diff --git a/Users/brendonovich/github.com/anomalyco/opencode/node_modules/solid-js/.bun-tag-b272f631c12927b0 b/.bun-tag-b272f631c12927b0 -new file mode 100644 -index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/dist/dev.cjs b/dist/dev.cjs -index 7104749486e4361e8c4ee7836a8046582cec7aa1..0501eb1ec5d13b81ecb13a5ac1a82db42502b976 100644 +index 7104749..dc3eac9 100644 --- a/dist/dev.cjs +++ b/dist/dev.cjs @@ -764,6 +764,8 @@ function runComputation(node, value, time) { @@ -17,8 +11,33 @@ index 7104749486e4361e8c4ee7836a8046582cec7aa1..0501eb1ec5d13b81ecb13a5ac1a82db4 Transition.sources.add(node); node.tValue = nextValue; } else node.value = nextValue; +@@ -987,18 +989,21 @@ function cleanNode(node) { + } + } + if (node.tOwned) { +- for (i = node.tOwned.length - 1; i >= 0; i--) cleanNode(node.tOwned[i]); ++ const tOwned = node.tOwned; + delete node.tOwned; ++ for (i = tOwned.length - 1; i >= 0; i--) cleanNode(tOwned[i]); + } + if (Transition && Transition.running && node.pure) { + reset(node, true); + } else if (node.owned) { +- for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]); ++ const owned = node.owned; + node.owned = null; ++ for (i = owned.length - 1; i >= 0; i--) cleanNode(owned[i]); + } + if (node.cleanups) { +- for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i](); ++ const cleanups = node.cleanups; + node.cleanups = null; ++ for (i = cleanups.length - 1; i >= 0; i--) cleanups[i](); + } + if (Transition && Transition.running) node.tState = 0;else node.state = 0; + delete node.sourceMap; diff --git a/dist/dev.js b/dist/dev.js -index ea5e4bc2fd4f0b3922a73d9134439529dc81339f..4b3ec07e624d20fdd23d6941a4fdde6d3a78cca3 100644 +index ea5e4bc..a2e2d59 100644 --- a/dist/dev.js +++ b/dist/dev.js @@ -762,6 +762,8 @@ function runComputation(node, value, time) { @@ -30,8 +49,75 @@ index ea5e4bc2fd4f0b3922a73d9134439529dc81339f..4b3ec07e624d20fdd23d6941a4fdde6d Transition.sources.add(node); node.tValue = nextValue; } else node.value = nextValue; +@@ -985,18 +987,21 @@ function cleanNode(node) { + } + } + if (node.tOwned) { +- for (i = node.tOwned.length - 1; i >= 0; i--) cleanNode(node.tOwned[i]); ++ const tOwned = node.tOwned; + delete node.tOwned; ++ for (i = tOwned.length - 1; i >= 0; i--) cleanNode(tOwned[i]); + } + if (Transition && Transition.running && node.pure) { + reset(node, true); + } else if (node.owned) { +- for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]); ++ const owned = node.owned; + node.owned = null; ++ for (i = owned.length - 1; i >= 0; i--) cleanNode(owned[i]); + } + if (node.cleanups) { +- for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i](); ++ const cleanups = node.cleanups; + node.cleanups = null; ++ for (i = cleanups.length - 1; i >= 0; i--) cleanups[i](); + } + if (Transition && Transition.running) node.tState = 0;else node.state = 0; + delete node.sourceMap; +diff --git a/dist/server.cjs b/dist/server.cjs +index e715309..188ba81 100644 +--- a/dist/server.cjs ++++ b/dist/server.cjs +@@ -127,12 +127,14 @@ function onCleanup(fn) { + } + function cleanNode(node) { + if (node.owned) { +- for (let i = 0; i < node.owned.length; i++) cleanNode(node.owned[i]); ++ const owned = node.owned; + node.owned = null; ++ for (let i = 0; i < owned.length; i++) cleanNode(owned[i]); + } + if (node.cleanups) { +- for (let i = 0; i < node.cleanups.length; i++) node.cleanups[i](); ++ const cleanups = node.cleanups; + node.cleanups = null; ++ for (let i = 0; i < cleanups.length; i++) cleanups[i](); + } + } + function catchError(fn, handler) { +diff --git a/dist/server.js b/dist/server.js +index d5f8803..320d9af 100644 +--- a/dist/server.js ++++ b/dist/server.js +@@ -125,12 +125,14 @@ function onCleanup(fn) { + } + function cleanNode(node) { + if (node.owned) { +- for (let i = 0; i < node.owned.length; i++) cleanNode(node.owned[i]); ++ const owned = node.owned; + node.owned = null; ++ for (let i = 0; i < owned.length; i++) cleanNode(owned[i]); + } + if (node.cleanups) { +- for (let i = 0; i < node.cleanups.length; i++) node.cleanups[i](); ++ const cleanups = node.cleanups; + node.cleanups = null; ++ for (let i = 0; i < cleanups.length; i++) cleanups[i](); + } + } + function catchError(fn, handler) { diff --git a/dist/solid.cjs b/dist/solid.cjs -index 7c133a2b254678a84fd61d719fbeffad766e1331..2f68c99f2698210cc0bac62f074cc8cd3beb2881 100644 +index 7c133a2..5ef1501 100644 --- a/dist/solid.cjs +++ b/dist/solid.cjs @@ -717,6 +717,8 @@ function runComputation(node, value, time) { @@ -43,8 +129,33 @@ index 7c133a2b254678a84fd61d719fbeffad766e1331..2f68c99f2698210cc0bac62f074cc8cd Transition.sources.add(node); node.tValue = nextValue; } else node.value = nextValue; +@@ -938,18 +940,21 @@ function cleanNode(node) { + } + } + if (node.tOwned) { +- for (i = node.tOwned.length - 1; i >= 0; i--) cleanNode(node.tOwned[i]); ++ const tOwned = node.tOwned; + delete node.tOwned; ++ for (i = tOwned.length - 1; i >= 0; i--) cleanNode(tOwned[i]); + } + if (Transition && Transition.running && node.pure) { + reset(node, true); + } else if (node.owned) { +- for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]); ++ const owned = node.owned; + node.owned = null; ++ for (i = owned.length - 1; i >= 0; i--) cleanNode(owned[i]); + } + if (node.cleanups) { +- for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i](); ++ const cleanups = node.cleanups; + node.cleanups = null; ++ for (i = cleanups.length - 1; i >= 0; i--) cleanups[i](); + } + if (Transition && Transition.running) node.tState = 0;else node.state = 0; + } diff --git a/dist/solid.js b/dist/solid.js -index 656fd26e7e5c794aa22df19c2377ff5c0591fc29..f08e9f5a7157c3506e5b6922fe2ef991335a80be 100644 +index 656fd26..6e0038c 100644 --- a/dist/solid.js +++ b/dist/solid.js @@ -715,6 +715,8 @@ function runComputation(node, value, time) { @@ -56,3 +167,28 @@ index 656fd26e7e5c794aa22df19c2377ff5c0591fc29..f08e9f5a7157c3506e5b6922fe2ef991 Transition.sources.add(node); node.tValue = nextValue; } else node.value = nextValue; +@@ -936,18 +938,21 @@ function cleanNode(node) { + } + } + if (node.tOwned) { +- for (i = node.tOwned.length - 1; i >= 0; i--) cleanNode(node.tOwned[i]); ++ const tOwned = node.tOwned; + delete node.tOwned; ++ for (i = tOwned.length - 1; i >= 0; i--) cleanNode(tOwned[i]); + } + if (Transition && Transition.running && node.pure) { + reset(node, true); + } else if (node.owned) { +- for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]); ++ const owned = node.owned; + node.owned = null; ++ for (i = owned.length - 1; i >= 0; i--) cleanNode(owned[i]); + } + if (node.cleanups) { +- for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i](); ++ const cleanups = node.cleanups; + node.cleanups = null; ++ for (i = cleanups.length - 1; i >= 0; i--) cleanups[i](); + } + if (Transition && Transition.running) node.tState = 0;else node.state = 0; + } From e1587588c46a52beff1de6d2735eec386aefa481 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 28 Jul 2026 11:09:22 +0000 Subject: [PATCH 062/129] chore: generate --- bun.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/bun.lock b/bun.lock index b4571f1314..084567c2e6 100644 --- a/bun.lock +++ b/bun.lock @@ -1083,6 +1083,7 @@ "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "pacote@21.5.0": "patches/pacote@21.5.0.patch", + "@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch", }, "overrides": { "@opentui/core": "catalog:", From 7336cc5a7bc89f0df96c27dd9659799c428189dc Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 28 Jul 2026 11:21:16 +0000 Subject: [PATCH 063/129] 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 1b85cf68b2..d1bd9c5131 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-RFek0QoEEjsgbqmTE/SxQAmPtYyzs0IPR2ugFn5Okrs=", - "aarch64-linux": "sha256-BmAxapY1YrAFn7mVq3/6A9+6Au5UIvSqBboHMkyJH3I=", - "aarch64-darwin": "sha256-Sx3bGWQqLlgoa/RudJxanjSzhFRNklckT2ffnO2I5F4=", - "x86_64-darwin": "sha256-CMOhiisHNowg06qadvgg4K+60zrynglwiT0qKYQ4NiA=" + "x86_64-linux": "sha256-wLjPbbeOF9OUNNGceYtEJNiBtF6TPjX/lnyMkdW170c=", + "aarch64-linux": "sha256-P7hrfVp3pILY/uAOdJweGg6IcXBLuU69yqko1qd1caw=", + "aarch64-darwin": "sha256-MJzCshCjkbypnW3yhmR8/zCadIopcHUfVT293N0FMY8=", + "x86_64-darwin": "sha256-6fR137gR66eI+VyvDkR2U51yoJ9BuvaK4YoMw+3E2A0=" } } From 017a5977d2107092007623e507fc5c6eb337d3b2 Mon Sep 17 00:00:00 2001 From: usrnk1 <7547651+usrnk1@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:50:58 +0200 Subject: [PATCH 064/129] feat(desktop): remove v2 vertical menu borders (#39317) --- packages/app/src/pages/home/home-projects-view.tsx | 5 ++--- packages/ui/src/v2/components/tabs-v2.css | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/app/src/pages/home/home-projects-view.tsx b/packages/app/src/pages/home/home-projects-view.tsx index 7ec01bd75f..d80d4a942f 100644 --- a/packages/app/src/pages/home/home-projects-view.tsx +++ b/packages/app/src/pages/home/home-projects-view.tsx @@ -482,7 +482,6 @@ function HomeProjectRow( class="pr-16 disabled:opacity-60" classList={{ "bg-v2-background-bg-layer-01 text-v2-text-text-base": sortable.isDragSource(), - "[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)]": sortable.isDragSource(), }} data-selected={props.selected ? "" : undefined} aria-current={props.selected ? "page" : undefined} @@ -590,9 +589,9 @@ function HomeProjectNavButton(props: JSX.ButtonHTMLAttributes class={` flex h-7 min-w-0 w-full shrink-0 cursor-default items-center gap-2 rounded-[6px] bg-transparent px-1.5 text-left text-v2-text-text-muted [font-weight:440] transition-[background-color,color,box-shadow] duration-[120ms] ease-in-out - hover:bg-v2-background-bg-layer-01 hover:text-v2-text-text-base hover:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)] + hover:bg-v2-background-bg-layer-01 hover:text-v2-text-text-base data-[selected]:bg-v2-background-bg-layer-03 data-[selected]:text-v2-text-text-base - data-[selected]:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)] data-[selected]:hover:bg-v2-background-bg-layer-03 + data-[selected]:hover:bg-v2-background-bg-layer-03 focus-visible:bg-v2-background-bg-layer-01 focus-visible:text-v2-text-text-base focus-visible:outline-none focus-visible:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)] ${local.class ?? ""} diff --git a/packages/ui/src/v2/components/tabs-v2.css b/packages/ui/src/v2/components/tabs-v2.css index 70abf78606..70daef1395 100644 --- a/packages/ui/src/v2/components/tabs-v2.css +++ b/packages/ui/src/v2/components/tabs-v2.css @@ -199,7 +199,6 @@ width: 100%; height: 28px; border-radius: 4px; - border: 0.5px solid transparent; box-sizing: border-box; color: var(--v2-text-text-muted); } @@ -222,5 +221,4 @@ [data-slot="tabs-v2-trigger-wrapper"]:has([data-selected]) { background-color: var(--v2-background-bg-layer-03); color: var(--v2-text-text-base); - border: 0.5px solid var(--v2-border-border-muted); } From 7edefb33471ec7c6f96731edadf6746bdab971d2 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:37:58 -0500 Subject: [PATCH 065/129] chore(mcp): upgrade client to 2.0.0 (#39369) Co-authored-by: Aiden Cline Co-authored-by: Aiden Cline --- bun.lock | 14 ++--- bunfig.toml | 2 +- package.json | 2 +- packages/opencode/package.json | 4 +- ...modelcontextprotocol%2Fclient@2.0.0.patch} | 52 +++++++++---------- 5 files changed, 37 insertions(+), 37 deletions(-) rename patches/{@modelcontextprotocol%2Fclient@2.0.0-beta.5.patch => @modelcontextprotocol%2Fclient@2.0.0.patch} (84%) diff --git a/bun.lock b/bun.lock index 084567c2e6..7a8f2c16a3 100644 --- a/bun.lock +++ b/bun.lock @@ -592,7 +592,7 @@ "@effect/platform-node": "catalog:", "@ff-labs/fff-bun": "0.9.4", "@gitlab/opencode-gitlab-auth": "1.3.3", - "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", @@ -666,7 +666,7 @@ }, "devDependencies": { "@babel/core": "7.28.4", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/core": "workspace:*", "@opencode-ai/http-recorder": "workspace:*", @@ -1071,10 +1071,10 @@ ], "patchedDependencies": { "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", - "@modelcontextprotocol/client@2.0.0-beta.5": "patches/@modelcontextprotocol%2Fclient@2.0.0-beta.5.patch", - "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", + "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", + "@modelcontextprotocol/client@2.0.0": "patches/@modelcontextprotocol%2Fclient@2.0.0.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", @@ -1830,11 +1830,11 @@ "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], - "@modelcontextprotocol/client": ["@modelcontextprotocol/client@2.0.0-beta.5", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0-beta.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "jose": "^6.1.3", "pkce-challenge": "^5.0.0", "zod": "^4.2.0" } }, "sha512-YuuNm5f2TMoFQRje1UqVP8TJRjijCXMz4ckvoVpx1cUXuBEmykWQ2d8R536pek6UKcXT41T5nWc4qR1JFIbEmg=="], + "@modelcontextprotocol/client": ["@modelcontextprotocol/client@2.0.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "jose": "^6.1.3", "pkce-challenge": "^5.0.0", "zod": "^4.2.0" } }, "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw=="], - "@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.0.0-beta.5", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-HKbY9XTbsDy1Y6r2I55TGE3JEapM0vg96e1MUmBIF9LGjos5gjhcIrTz1yvBPLg2aFKHjwhUAQfRdrCEnPxNew=="], + "@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.0.0", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA=="], - "@modelcontextprotocol/server": ["@modelcontextprotocol/server@2.0.0-beta.5", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0-beta.5", "zod": "^4.2.0" } }, "sha512-i1E5l75rQKsgY/AKAIspgMBH1vEL7dqiK7tHr0L+raYcb0SWOziqNGJXGIG6NY4AlXDWIKGJQGB7Nqfs3oUi5g=="], + "@modelcontextprotocol/server": ["@modelcontextprotocol/server@2.0.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0", "zod": "^4.2.0" } }, "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw=="], "@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="], diff --git a/bunfig.toml b/bunfig.toml index c506ff57c4..649db19f1d 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -2,7 +2,7 @@ exact = true # Only install newly resolved package versions published at least 3 days ago. minimumReleaseAge = 259200 -minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish"] +minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@modelcontextprotocol/client", "@modelcontextprotocol/core", "@modelcontextprotocol/server", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish"] [test] root = "./do-not-run-tests-from-root" diff --git a/package.json b/package.json index e7e081a4d3..30630fdf2f 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,6 @@ "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", - "@modelcontextprotocol/client@2.0.0-beta.5": "patches/@modelcontextprotocol%2Fclient@2.0.0-beta.5.patch" + "@modelcontextprotocol/client@2.0.0": "patches/@modelcontextprotocol%2Fclient@2.0.0.patch" } } diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 293a678e9b..d2155eb4ba 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "@babel/core": "7.28.4", - "@modelcontextprotocol/server": "2.0.0-beta.5", + "@modelcontextprotocol/server": "2.0.0", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/core": "workspace:*", "@opencode-ai/http-recorder": "workspace:*", @@ -81,7 +81,7 @@ "@effect/platform-node": "catalog:", "@ff-labs/fff-bun": "0.9.4", "@gitlab/opencode-gitlab-auth": "1.3.3", - "@modelcontextprotocol/client": "2.0.0-beta.5", + "@modelcontextprotocol/client": "2.0.0", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", diff --git a/patches/@modelcontextprotocol%2Fclient@2.0.0-beta.5.patch b/patches/@modelcontextprotocol%2Fclient@2.0.0.patch similarity index 84% rename from patches/@modelcontextprotocol%2Fclient@2.0.0-beta.5.patch rename to patches/@modelcontextprotocol%2Fclient@2.0.0.patch index 4205f15831..833adea8a9 100644 --- a/patches/@modelcontextprotocol%2Fclient@2.0.0-beta.5.patch +++ b/patches/@modelcontextprotocol%2Fclient@2.0.0.patch @@ -1,8 +1,8 @@ diff --git a/dist/index.cjs b/dist/index.cjs -index 1c43bac25a1037416fdf2ddfb4534ba0897a2e69..7e2108326f368ccbc48897195d57e370a4553855 100644 +index 635f1c0..9214f0b 100644 --- a/dist/index.cjs +++ b/dist/index.cjs -@@ -3154,6 +3154,7 @@ var Client = class extends require_src.Protocol { +@@ -3211,6 +3211,7 @@ var Client = class extends require_src.Protocol { */ async _connectPlainLegacy(transport, options) { await super.connect(transport); @@ -10,7 +10,7 @@ index 1c43bac25a1037416fdf2ddfb4534ba0897a2e69..7e2108326f368ccbc48897195d57e370 if (transport.sessionId !== void 0) { const negotiatedProtocolVersion = this._negotiatedProtocolVersion; if (negotiatedProtocolVersion !== void 0) transport.setProtocolVersion?.(negotiatedProtocolVersion); -@@ -3170,6 +3171,7 @@ var Client = class extends require_src.Protocol { +@@ -3227,6 +3228,7 @@ var Client = class extends require_src.Protocol { * the handshake; its completion sets the negotiated (legacy) version. */ async _legacyHandshake(transport, options) { @@ -18,7 +18,7 @@ index 1c43bac25a1037416fdf2ddfb4534ba0897a2e69..7e2108326f368ccbc48897195d57e370 const legacyVersions = require_src.legacyProtocolVersions(this._supportedProtocolVersions); try { const offeredVersion = legacyVersions[0]; -@@ -3208,6 +3210,7 @@ var Client = class extends require_src.Protocol { +@@ -3265,6 +3267,7 @@ var Client = class extends require_src.Protocol { await super.connect(transport); const negotiatedProtocolVersion = this._negotiatedProtocolVersion; if (negotiatedProtocolVersion !== void 0 && transport.setProtocolVersion) transport.setProtocolVersion(negotiatedProtocolVersion); @@ -26,7 +26,7 @@ index 1c43bac25a1037416fdf2ddfb4534ba0897a2e69..7e2108326f368ccbc48897195d57e370 return; } this._resetConnectionState(); -@@ -5211,10 +5214,32 @@ var StreamableHTTPClientTransport = class { +@@ -5294,10 +5297,32 @@ var StreamableHTTPClientTransport = class { } } async send(message, options) { @@ -61,7 +61,7 @@ index 1c43bac25a1037416fdf2ddfb4534ba0897a2e69..7e2108326f368ccbc48897195d57e370 const { resumptionToken, onresumptiontoken } = options || {}; if (resumptionToken) { this._startOrAuthSse({ -@@ -5226,8 +5251,8 @@ var StreamableHTTPClientTransport = class { +@@ -5309,8 +5334,8 @@ var StreamableHTTPClientTransport = class { } const headers = await this._commonHeaders(); this._applyBodyDerivedHeaders(headers, message); @@ -71,7 +71,7 @@ index 1c43bac25a1037416fdf2ddfb4534ba0897a2e69..7e2108326f368ccbc48897195d57e370 if (options?.headers !== void 0) for (const [name, value] of Object.entries(options.headers)) { if (RESERVED_REQUEST_HEADER_NAMES.has(name.toLowerCase())) continue; headers.set(name, value); -@@ -5249,8 +5274,14 @@ var StreamableHTTPClientTransport = class { +@@ -5332,8 +5357,14 @@ var StreamableHTTPClientTransport = class { signal }; const response = await (this._fetch ?? fetch)(this._url, init); @@ -87,29 +87,29 @@ index 1c43bac25a1037416fdf2ddfb4534ba0897a2e69..7e2108326f368ccbc48897195d57e370 if (response.status === 401 && this._authProvider) { if (response.headers.has("www-authenticate")) { const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response); -@@ -5264,7 +5295,7 @@ var StreamableHTTPClientTransport = class { - fetchFn: this._fetchWithInit - }); +@@ -5351,7 +5382,7 @@ var StreamableHTTPClientTransport = class { + throw markAuthSeamEscape(error); + } await response.text?.().catch(() => {}); - return this._send(message, options, true, stepUpRetries); + return this._send(message, options, true, stepUpRetries, isSessionRetry); } await response.text?.().catch(() => {}); - if (isAuthRetry) throw new require_src.SdkHttpError(require_src.SdkErrorCode.ClientHttpAuthentication, "Server returned 401 after re-authentication", { -@@ -5284,7 +5315,7 @@ var StreamableHTTPClientTransport = class { + if (isAuthRetry) throw markAuthSeamEscape(new require_src.SdkHttpError(require_src.SdkErrorCode.ClientHttpAuthentication, "Server returned 401 after re-authentication", { +@@ -5371,7 +5402,7 @@ var StreamableHTTPClientTransport = class { statusText: response.statusText, text - }, stepUpRetries) !== "AUTHORIZED") throw new UnauthorizedError(); + }, stepUpRetries) !== "AUTHORIZED") throw markAuthSeamEscape(new UnauthorizedError()); - return this._send(message, options, isAuthRetry, stepUpRetries + 1); + return this._send(message, options, isAuthRetry, stepUpRetries + 1, isSessionRetry); } } if (response.status === 400 && typeof text === "string" && this._isModernEnvelopedRequest(message)) try { diff --git a/dist/index.mjs b/dist/index.mjs -index 77e2389913cb5c5c2b047f95d990ab2892bef923..4b5e4ff2869189d600ca644488a7749668c39747 100644 +index f02ce3c..0a5a649 100644 --- a/dist/index.mjs +++ b/dist/index.mjs -@@ -3151,6 +3151,7 @@ var Client = class extends Protocol { +@@ -3208,6 +3208,7 @@ var Client = class extends Protocol { */ async _connectPlainLegacy(transport, options) { await super.connect(transport); @@ -117,7 +117,7 @@ index 77e2389913cb5c5c2b047f95d990ab2892bef923..4b5e4ff2869189d600ca644488a77496 if (transport.sessionId !== void 0) { const negotiatedProtocolVersion = this._negotiatedProtocolVersion; if (negotiatedProtocolVersion !== void 0) transport.setProtocolVersion?.(negotiatedProtocolVersion); -@@ -3167,6 +3168,7 @@ var Client = class extends Protocol { +@@ -3224,6 +3225,7 @@ var Client = class extends Protocol { * the handshake; its completion sets the negotiated (legacy) version. */ async _legacyHandshake(transport, options) { @@ -125,7 +125,7 @@ index 77e2389913cb5c5c2b047f95d990ab2892bef923..4b5e4ff2869189d600ca644488a77496 const legacyVersions = legacyProtocolVersions(this._supportedProtocolVersions); try { const offeredVersion = legacyVersions[0]; -@@ -3205,6 +3207,7 @@ var Client = class extends Protocol { +@@ -3262,6 +3264,7 @@ var Client = class extends Protocol { await super.connect(transport); const negotiatedProtocolVersion = this._negotiatedProtocolVersion; if (negotiatedProtocolVersion !== void 0 && transport.setProtocolVersion) transport.setProtocolVersion(negotiatedProtocolVersion); @@ -133,7 +133,7 @@ index 77e2389913cb5c5c2b047f95d990ab2892bef923..4b5e4ff2869189d600ca644488a77496 return; } this._resetConnectionState(); -@@ -5208,10 +5211,32 @@ var StreamableHTTPClientTransport = class { +@@ -5291,10 +5294,32 @@ var StreamableHTTPClientTransport = class { } } async send(message, options) { @@ -168,7 +168,7 @@ index 77e2389913cb5c5c2b047f95d990ab2892bef923..4b5e4ff2869189d600ca644488a77496 const { resumptionToken, onresumptiontoken } = options || {}; if (resumptionToken) { this._startOrAuthSse({ -@@ -5223,8 +5248,8 @@ var StreamableHTTPClientTransport = class { +@@ -5306,8 +5331,8 @@ var StreamableHTTPClientTransport = class { } const headers = await this._commonHeaders(); this._applyBodyDerivedHeaders(headers, message); @@ -178,7 +178,7 @@ index 77e2389913cb5c5c2b047f95d990ab2892bef923..4b5e4ff2869189d600ca644488a77496 if (options?.headers !== void 0) for (const [name, value] of Object.entries(options.headers)) { if (RESERVED_REQUEST_HEADER_NAMES.has(name.toLowerCase())) continue; headers.set(name, value); -@@ -5246,8 +5271,14 @@ var StreamableHTTPClientTransport = class { +@@ -5329,8 +5354,14 @@ var StreamableHTTPClientTransport = class { signal }; const response = await (this._fetch ?? fetch)(this._url, init); @@ -194,19 +194,19 @@ index 77e2389913cb5c5c2b047f95d990ab2892bef923..4b5e4ff2869189d600ca644488a77496 if (response.status === 401 && this._authProvider) { if (response.headers.has("www-authenticate")) { const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response); -@@ -5261,7 +5292,7 @@ var StreamableHTTPClientTransport = class { - fetchFn: this._fetchWithInit - }); +@@ -5348,7 +5379,7 @@ var StreamableHTTPClientTransport = class { + throw markAuthSeamEscape(error); + } await response.text?.().catch(() => {}); - return this._send(message, options, true, stepUpRetries); + return this._send(message, options, true, stepUpRetries, isSessionRetry); } await response.text?.().catch(() => {}); - if (isAuthRetry) throw new SdkHttpError(SdkErrorCode.ClientHttpAuthentication, "Server returned 401 after re-authentication", { -@@ -5281,7 +5312,7 @@ var StreamableHTTPClientTransport = class { + if (isAuthRetry) throw markAuthSeamEscape(new SdkHttpError(SdkErrorCode.ClientHttpAuthentication, "Server returned 401 after re-authentication", { +@@ -5368,7 +5399,7 @@ var StreamableHTTPClientTransport = class { statusText: response.statusText, text - }, stepUpRetries) !== "AUTHORIZED") throw new UnauthorizedError(); + }, stepUpRetries) !== "AUTHORIZED") throw markAuthSeamEscape(new UnauthorizedError()); - return this._send(message, options, isAuthRetry, stepUpRetries + 1); + return this._send(message, options, isAuthRetry, stepUpRetries + 1, isSessionRetry); } From 982a9044c515482e7792039be1db9c71cb572745 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:53:35 -0500 Subject: [PATCH 066/129] fix(mcp): restore legacy SDK compatibility (#39373) --- bun.lock | 83 ++- bunfig.toml | 2 +- package.json | 4 +- packages/opencode/package.json | 3 +- packages/opencode/src/cli/cmd/mcp.ts | 151 +++-- packages/opencode/src/mcp/auth.ts | 4 - packages/opencode/src/mcp/catalog.ts | 135 ++-- packages/opencode/src/mcp/index.ts | 82 +-- packages/opencode/src/mcp/oauth-callback.ts | 12 +- packages/opencode/src/mcp/oauth-provider.ts | 85 +-- .../routes/instance/httpapi/groups/mcp.ts | 1 - .../routes/instance/httpapi/handlers/mcp.ts | 2 +- packages/opencode/src/session/tools.ts | 2 +- packages/opencode/src/tool/code-mode.ts | 25 +- .../test/fixture/mcp-lifecycle-stdio.ts | 7 +- .../test/fixture/mcp-session-recovery.ts | 14 +- packages/opencode/test/mcp/catalog.test.ts | 66 +- packages/opencode/test/mcp/headers.test.ts | 25 +- packages/opencode/test/mcp/lifecycle.test.ts | 31 +- .../test/mcp/oauth-auto-connect.test.ts | 8 +- .../opencode/test/mcp/oauth-browser.test.ts | 6 +- .../opencode/test/mcp/oauth-callback.test.ts | 2 +- .../opencode/test/mcp/oauth-provider.test.ts | 41 ++ .../test/mcp/session-recovery.test.ts | 20 - .../test/tool/code-mode-integration.test.ts | 17 +- packages/opencode/test/tool/code-mode.test.ts | 2 +- packages/opencode/test/tool/registry.test.ts | 2 +- packages/sdk/js/src/v2/gen/sdk.gen.ts | 2 - packages/sdk/js/src/v2/gen/types.gen.ts | 1 - ...@modelcontextprotocol%2Fclient@2.0.0.patch | 214 ------ .../@modelcontextprotocol%2Fsdk@1.29.0.patch | 629 ++++++++++++++++++ 31 files changed, 1082 insertions(+), 596 deletions(-) delete mode 100644 patches/@modelcontextprotocol%2Fclient@2.0.0.patch create mode 100644 patches/@modelcontextprotocol%2Fsdk@1.29.0.patch diff --git a/bun.lock b/bun.lock index 7a8f2c16a3..96458854bc 100644 --- a/bun.lock +++ b/bun.lock @@ -592,7 +592,7 @@ "@effect/platform-node": "catalog:", "@ff-labs/fff-bun": "0.9.4", "@gitlab/opencode-gitlab-auth": "1.3.3", - "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/sdk": "1.29.0", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", @@ -666,7 +666,6 @@ }, "devDependencies": { "@babel/core": "7.28.4", - "@modelcontextprotocol/server": "2.0.0", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/core": "workspace:*", "@opencode-ai/http-recorder": "workspace:*", @@ -1073,8 +1072,8 @@ "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", + "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", - "@modelcontextprotocol/client@2.0.0": "patches/@modelcontextprotocol%2Fclient@2.0.0.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", @@ -1676,6 +1675,8 @@ "@hey-api/types": ["@hey-api/types@0.1.2", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="], + "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], + "@hono/standard-validator": ["@hono/standard-validator@0.2.0", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-pFq0UVAnjzXcDAgqFpDeVL3MOUPrlIh/kPqBDvbCYoThVhhS+Vf37VcdsakdOFFGiqoiYVxp3LifXFhGhp/rgQ=="], "@ibm/plex": ["@ibm/plex@6.4.1", "", { "dependencies": { "@ibm/telemetry-js": "^1.5.1" } }, "sha512-fnsipQywHt3zWvsnlyYKMikcVI7E2fEwpiPnIHFqlbByXVfQfANAAeJk1IV4mNnxhppUIDlhU0TzwYwL++Rn2g=="], @@ -1830,11 +1831,7 @@ "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], - "@modelcontextprotocol/client": ["@modelcontextprotocol/client@2.0.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "jose": "^6.1.3", "pkce-challenge": "^5.0.0", "zod": "^4.2.0" } }, "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw=="], - - "@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.0.0", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA=="], - - "@modelcontextprotocol/server": ["@modelcontextprotocol/server@2.0.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0", "zod": "^4.2.0" } }, "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], "@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="], @@ -3394,7 +3391,7 @@ "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], - "content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -3406,6 +3403,8 @@ "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + "crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="], "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], @@ -3710,6 +3709,8 @@ "express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], + "express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="], + "expressive-code": ["expressive-code@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "@expressive-code/plugin-frames": "^0.41.7", "@expressive-code/plugin-shiki": "^0.41.7", "@expressive-code/plugin-text-markers": "^0.41.7" } }, "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA=="], "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], @@ -4094,6 +4095,8 @@ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], @@ -5002,6 +5005,8 @@ "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -5950,13 +5955,15 @@ "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "@modelcontextprotocol/client/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + "@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=="], - "@modelcontextprotocol/client/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@modelcontextprotocol/sdk/hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="], - "@modelcontextprotocol/core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@modelcontextprotocol/sdk/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], - "@modelcontextprotocol/server/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "@modelcontextprotocol/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], @@ -6232,8 +6239,6 @@ "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], - "body-parser/content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], @@ -6316,8 +6321,6 @@ "execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], - "express/content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -6466,6 +6469,8 @@ "roarr/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], + "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], @@ -6794,28 +6799,58 @@ "@malept/flatpak-bundler/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "@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=="], + + "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], + + "@modelcontextprotocol/sdk/express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "@modelcontextprotocol/sdk/express/send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "@modelcontextprotocol/sdk/express/type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], + "@octokit/auth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-app/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@octokit/auth-app/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/auth-app/@octokit/request-error/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], "@octokit/auth-oauth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-oauth-app/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + "@octokit/auth-oauth-app/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/auth-oauth-app/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/auth-oauth-device/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-oauth-device/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + "@octokit/auth-oauth-device/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/auth-oauth-device/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/auth-oauth-user/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/auth-oauth-user/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + "@octokit/auth-oauth-user/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/auth-oauth-user/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], @@ -6828,10 +6863,14 @@ "@octokit/graphql/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@octokit/graphql/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/graphql/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="], "@octokit/oauth-methods/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], + "@octokit/oauth-methods/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/oauth-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/plugin-paginate-rest/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], @@ -7340,6 +7379,10 @@ "@jsx-email/cli/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@modelcontextprotocol/sdk/express/type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + + "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/auth-app/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], @@ -7348,14 +7391,20 @@ "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], + "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/plugin-paginate-rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], + "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], + "@octokit/rest/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@opencode-ai/core/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], diff --git a/bunfig.toml b/bunfig.toml index 649db19f1d..c506ff57c4 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -2,7 +2,7 @@ exact = true # Only install newly resolved package versions published at least 3 days ago. minimumReleaseAge = 259200 -minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@modelcontextprotocol/client", "@modelcontextprotocol/core", "@modelcontextprotocol/server", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish"] +minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish"] [test] root = "./do-not-run-tests-from-root" diff --git a/package.json b/package.json index 30630fdf2f..e479eb7406 100644 --- a/package.json +++ b/package.json @@ -155,8 +155,8 @@ "pacote@21.5.0": "patches/pacote@21.5.0.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", + "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", - "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", - "@modelcontextprotocol/client@2.0.0": "patches/@modelcontextprotocol%2Fclient@2.0.0.patch" + "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch" } } diff --git a/packages/opencode/package.json b/packages/opencode/package.json index d2155eb4ba..8f7af65a8f 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -30,7 +30,6 @@ }, "devDependencies": { "@babel/core": "7.28.4", - "@modelcontextprotocol/server": "2.0.0", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/core": "workspace:*", "@opencode-ai/http-recorder": "workspace:*", @@ -81,7 +80,7 @@ "@effect/platform-node": "catalog:", "@ff-labs/fff-bun": "0.9.4", "@gitlab/opencode-gitlab-auth": "1.3.3", - "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/sdk": "1.29.0", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index c9f74e2bc0..c2d2ee2f3b 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -2,10 +2,13 @@ import { cmd } from "./cmd" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { effectCmd } from "../effect-cmd" import { Cause } from "effect" -import { Client, StreamableHTTPClientTransport, UnauthorizedError } from "@modelcontextprotocol/client" +import { Client } from "@modelcontextprotocol/sdk/client/index.js" +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" +import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js" +import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js" import * as prompts from "@clack/prompts" import { UI } from "../ui" -import { CLIENT_OPTIONS, MCP } from "../../mcp" +import { MCP } from "../../mcp" import { McpAuth } from "../../mcp/auth" import { McpOAuthProvider } from "../../mcp/oauth-provider" import { Config } from "@/config/config" @@ -728,53 +731,107 @@ export const McpDebugCommand = effectCmd({ const spinner = prompts.spinner() spinner.start("Testing connection...") - const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined - let authorizationUrl: URL | undefined - const authProvider = new McpOAuthProvider( - serverName, - serverConfig.url, - { - clientId: oauthConfig?.clientId, - clientSecret: oauthConfig?.clientSecret, - scope: oauthConfig?.scope, - callbackPort: oauthConfig?.callbackPort, - redirectUri: oauthConfig?.redirectUri, - }, - { - onRedirect: async (url) => { - authorizationUrl = url - }, - }, - auth, - ) - const transport = new StreamableHTTPClientTransport(new URL(serverConfig.url), { - authProvider, - requestInit: serverConfig.headers ? { headers: serverConfig.headers } : undefined, - }) - const client = new Client({ name: "opencode-debug", version: InstallationVersion }, CLIENT_OPTIONS) - + // Test basic HTTP connectivity first try { - await client.connect(transport) - spinner.stop("SDK connection successful") - prompts.log.success( - `Connected using MCP ${client.getNegotiatedProtocolVersion() ?? "unknown"} (${client.getProtocolEra() ?? "unknown"})`, - ) - const serverInfo = client.getServerVersion() - if (serverInfo) prompts.log.info(`Server info: ${JSON.stringify(serverInfo)}`) - } catch (error) { - if (error instanceof UnauthorizedError) { - spinner.stop("OAuth required") - prompts.log.info(`OAuth flow triggered: ${error.message}`) - if (authorizationUrl) prompts.log.info(`Authorization URL: ${authorizationUrl}`) - const clientInfo = await authProvider.clientInformation() - if (clientInfo) prompts.log.info(`Client ID available: ${clientInfo.client_id}`) - if (!clientInfo) prompts.log.info("No client ID - dynamic registration will be attempted") - } else { - spinner.stop("Connection failed", 1) - prompts.log.error(`Error: ${error instanceof Error ? error.message : String(error)}`) + const response = await fetch(serverConfig.url, { + method: "POST", + headers: { + ...serverConfig.headers, + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "initialize", + params: { + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: {}, + clientInfo: { name: "opencode-debug", version: InstallationVersion }, + }, + id: 1, + }), + }) + + spinner.stop(`HTTP response: ${response.status} ${response.statusText}`) + + // Check for WWW-Authenticate header + const wwwAuth = response.headers.get("www-authenticate") + if (wwwAuth) { + prompts.log.info(`WWW-Authenticate: ${wwwAuth}`) } - } finally { - await client.close().catch(() => {}) + + if (response.status === 401) { + prompts.log.info("Initial unauthenticated check returned 401, so this server requires OAuth") + + // Try to discover OAuth metadata + const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined + const authProvider = new McpOAuthProvider( + serverName, + serverConfig.url, + { + clientId: oauthConfig?.clientId, + clientSecret: oauthConfig?.clientSecret, + scope: oauthConfig?.scope, + redirectUri: oauthConfig?.redirectUri, + }, + { + onRedirect: async () => {}, + }, + auth, + ) + + prompts.log.info("Testing OAuth flow (without completing authorization)...") + + // Try creating transport with auth provider to trigger discovery + const transport = new StreamableHTTPClientTransport(new URL(serverConfig.url), { + authProvider, + requestInit: serverConfig.headers ? { headers: serverConfig.headers } : undefined, + }) + + try { + const client = new Client({ + name: "opencode-debug", + version: InstallationVersion, + }) + await client.connect(transport) + prompts.log.success("Connection successful (already authenticated)") + await client.close() + } catch (error) { + if (error instanceof UnauthorizedError) { + prompts.log.info(`OAuth flow triggered: ${error.message}`) + + // Check if dynamic registration would be attempted + const clientInfo = await authProvider.clientInformation() + if (clientInfo) { + prompts.log.info(`Client ID available: ${clientInfo.client_id}`) + } else { + prompts.log.info("No client ID - dynamic registration will be attempted") + } + } else { + prompts.log.error(`Connection error: ${error instanceof Error ? error.message : String(error)}`) + } + } + } else if (response.status >= 200 && response.status < 300) { + prompts.log.success("Server responded successfully (no auth required or already authenticated)") + const body = await response.text() + try { + const json = JSON.parse(body) + if (json.result?.serverInfo) { + prompts.log.info(`Server info: ${JSON.stringify(json.result.serverInfo)}`) + } + } catch { + // Not JSON, ignore + } + } else { + prompts.log.warn(`Unexpected status: ${response.status}`) + const body = await response.text().catch(() => "") + if (body) { + prompts.log.info(`Response body: ${body.substring(0, 500)}`) + } + } + } catch (error) { + spinner.stop("Connection failed", 1) + prompts.log.error(`Error: ${error instanceof Error ? error.message : String(error)}`) } prompts.outro("Debug complete") diff --git a/packages/opencode/src/mcp/auth.ts b/packages/opencode/src/mcp/auth.ts index 543b4fb0e3..808aa30296 100644 --- a/packages/opencode/src/mcp/auth.ts +++ b/packages/opencode/src/mcp/auth.ts @@ -11,7 +11,6 @@ export const Tokens = Schema.Struct({ refreshToken: Schema.mutableKey(Schema.optional(Schema.String)), expiresAt: Schema.mutableKey(Schema.optional(Schema.Number)), scope: Schema.mutableKey(Schema.optional(Schema.String)), - issuer: Schema.mutableKey(Schema.optional(Schema.String)), }) export type Tokens = Schema.Schema.Type @@ -20,9 +19,6 @@ export const ClientInfo = Schema.Struct({ clientSecret: Schema.mutableKey(Schema.optional(Schema.String)), clientIdIssuedAt: Schema.mutableKey(Schema.optional(Schema.Number)), clientSecretExpiresAt: Schema.mutableKey(Schema.optional(Schema.Number)), - redirectUris: Schema.mutableKey(Schema.optional(Schema.Array(Schema.String))), - issuer: Schema.mutableKey(Schema.optional(Schema.String)), - configPreRegistered: Schema.mutableKey(Schema.optional(Schema.Boolean)), }) export type ClientInfo = Schema.Schema.Type diff --git a/packages/opencode/src/mcp/catalog.ts b/packages/opencode/src/mcp/catalog.ts index 226cd6bb25..3f985eeb94 100644 --- a/packages/opencode/src/mcp/catalog.ts +++ b/packages/opencode/src/mcp/catalog.ts @@ -1,57 +1,77 @@ -import { Client, type CallToolResult, type Tool as MCPToolDef } from "@modelcontextprotocol/client" +import { Client } from "@modelcontextprotocol/sdk/client/index.js" +import { + CallToolResultSchema, + ListToolsResultSchema, + ToolSchema, + type Tool as MCPToolDef, +} from "@modelcontextprotocol/sdk/types.js" import { dynamicTool, jsonSchema, type JSONSchema7, type Tool } from "ai" import { Effect } from "effect" const DEFAULT_TIMEOUT = 30_000 +const MAX_LIST_PAGES = 1_000 -export interface McpTool { - readonly def: MCPToolDef - readonly client: Client - readonly timeout?: number -} +const TolerantListToolsResultSchema = ListToolsResultSchema.extend({ + tools: ToolSchema.omit({ outputSchema: true }).array(), +}) -export async function callTool( - tool: McpTool, - args: Record, - signal?: AbortSignal, -): Promise { - const result = await tool.client.callTool( - { name: tool.def.name, arguments: args }, - { - resetTimeoutOnProgress: true, - signal, - timeout: tool.timeout, - // The MCP SDK only sends a progress token when this hook is present, enabling timeout resets. - onprogress: () => {}, - }, - ) - if (result.isError) - throw new Error( - result.content - .flatMap((item) => (item.type === "text" ? [item.text] : [])) - .filter((text) => text.trim()) - .join("\n\n") || "MCP tool returned an error", - ) - return result +export async function paginate( + list: (cursor?: string) => Promise, + items: (result: R) => T[], +) { + const result: T[] = [] + const cursors = new Set() + let cursor: string | undefined + + for (let page = 0; page < MAX_LIST_PAGES; page++) { + const page = await list(cursor) + result.push(...items(page)) + if (page.nextCursor === undefined) return result + if (cursors.has(page.nextCursor)) throw new Error(`MCP list returned duplicate cursor: ${page.nextCursor}`) + cursors.add(page.nextCursor) + cursor = page.nextCursor + } + + throw new Error(`MCP list exceeded ${MAX_LIST_PAGES} pages`) } export function defs(client: Client, timeout?: number) { return listTools(client, timeout ?? DEFAULT_TIMEOUT).pipe(Effect.catch(() => Effect.void)) } -export function convertTool(tool: McpTool): Tool { +export function convertTool(mcpTool: MCPToolDef, client: Client, timeout?: number): Tool { const inputSchema: JSONSchema7 = { - ...(tool.def.inputSchema as JSONSchema7), + ...(mcpTool.inputSchema as JSONSchema7), type: "object", - properties: (tool.def.inputSchema.properties ?? {}) as JSONSchema7["properties"], + properties: (mcpTool.inputSchema.properties ?? {}) as JSONSchema7["properties"], additionalProperties: false, } return dynamicTool({ - description: tool.def.description ?? "", + description: mcpTool.description ?? "", inputSchema: jsonSchema(inputSchema), execute: async (args: unknown, options) => { - const result = await callTool(tool, (args || {}) as Record, options.abortSignal) + const result = await client.callTool( + { + name: mcpTool.name, + arguments: (args || {}) as Record, + }, + CallToolResultSchema, + { + resetTimeoutOnProgress: true, + signal: options.abortSignal, + timeout, + // The MCP SDK only sends a progress token when this hook is present, enabling timeout resets. + onprogress: () => {}, + }, + ) + if (result.isError) + throw new Error( + result.content + .flatMap((item) => (item.type === "text" ? [item.text] : [])) + .filter((text) => text.trim()) + .join("\n\n") || "MCP tool returned an error", + ) if (result.content.length > 0 || result.structuredContent === undefined || result.structuredContent === null) return result return { @@ -98,26 +118,53 @@ export const sanitize = (value: string) => value.replace(/[^a-zA-Z0-9_-]/g, "_") export const toolName = (clientName: string, name: string) => sanitize(clientName) + "_" + sanitize(name) -export async function prompts(client: Client, timeout?: number) { - if (!client.getServerCapabilities()?.prompts) return [] - return (await client.listPrompts(undefined, { timeout })).prompts +export function prompts(client: Client, timeout?: number) { + if (!client.getServerCapabilities()?.prompts) return Promise.resolve([]) + return paginate( + (cursor) => client.listPrompts(cursor === undefined ? undefined : { cursor }, { timeout }), + (result) => result.prompts, + ) } -export async function resources(client: Client, timeout?: number) { - if (!client.getServerCapabilities()?.resources) return [] - return (await client.listResources(undefined, { timeout })).resources +export function resources(client: Client, timeout?: number) { + if (!client.getServerCapabilities()?.resources) return Promise.resolve([]) + return paginate( + (cursor) => client.listResources(cursor === undefined ? undefined : { cursor }, { timeout }), + (result) => result.resources, + ) } -export async function resourceTemplates(client: Client, timeout?: number) { - if (!client.getServerCapabilities()?.resources) return [] - return (await client.listResourceTemplates(undefined, { timeout })).resourceTemplates +export function resourceTemplates(client: Client, timeout?: number) { + if (!client.getServerCapabilities()?.resources) return Promise.resolve([]) + return paginate( + (cursor) => client.listResourceTemplates(cursor === undefined ? undefined : { cursor }, { timeout }), + (result) => result.resourceTemplates, + ) } function listTools(client: Client, timeout: number) { return Effect.tryPromise({ - try: async () => (await client.listTools(undefined, { timeout })).tools, + try: () => + paginate( + async (cursor) => { + const params = cursor === undefined ? undefined : { cursor } + try { + return await client.listTools(params, { timeout }) + } catch (error) { + if (!(error instanceof Error) || !isOutputSchemaValidationError(error)) throw error + return client.request({ method: "tools/list", params }, TolerantListToolsResultSchema, { timeout }) + } + }, + (result) => result.tools, + ), catch: (error) => (error instanceof Error ? error : new Error(String(error))), }) } +function isOutputSchemaValidationError(error: Error) { + return /can't resolve reference|resolves to more than one schema|outputSchema|schema.*reference|reference.*schema/i.test( + error.message, + ) +} + export * as McpCatalog from "./catalog" diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 939c442047..05f12fa2ee 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -3,18 +3,18 @@ import { pathToFileURL } from "node:url" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { serviceUse } from "@opencode-ai/core/effect/service-use" +import { Client, type ClientOptions } from "@modelcontextprotocol/sdk/client/index.js" +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" +import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js" import { - Client, - type ClientOptions, - StreamableHTTPClientTransport, - SSEClientTransport, - UnauthorizedError, - RegistrationRejectedError, - SdkHttpError, + ListRootsRequestSchema, type LoggingMessageNotification, + LoggingMessageNotificationSchema, type Tool as MCPToolDef, -} from "@modelcontextprotocol/client" -import { StdioClientTransport } from "@modelcontextprotocol/client/stdio" + ToolListChangedNotificationSchema, +} from "@modelcontextprotocol/sdk/types.js" import { Config } from "@/config/config" import { ConfigMCPV1 } from "@opencode-ai/core/v1/config/mcp" import { NamedError } from "@opencode-ai/core/util/error" @@ -36,7 +36,7 @@ import { McpEvent } from "@opencode-ai/schema/mcp-event" import { McpBrowser } from "./browser" const DEFAULT_TIMEOUT = 30_000 -export const CLIENT_OPTIONS = { +const CLIENT_OPTIONS = { capabilities: { // https://github.com/anomalyco/opencode/issues/11948 // sampling: {}, @@ -47,8 +47,6 @@ export const CLIENT_OPTIONS = { // https://github.com/anomalyco/opencode/issues/28567 // tasks: {}, }, - versionNegotiation: { mode: "auto" }, - listMaxPages: 1_000, } satisfies ClientOptions export const Resource = Schema.Struct({ @@ -72,19 +70,13 @@ export class NotFoundError extends Schema.TaggedErrorClass()("MCP name: Schema.String, }) {} -type MCPClient = Client & { onToolsChanged?: (error: Error | null) => void } +type MCPClient = Client function createClient(directory: string) { - const client: MCPClient = new Client( - { name: "opencode", version: InstallationVersion }, - { - ...CLIENT_OPTIONS, - listChanged: { - tools: { autoRefresh: false, onChanged: (error) => client.onToolsChanged?.(error) }, - }, - }, + const client = new Client({ name: "opencode", version: InstallationVersion }, CLIENT_OPTIONS) + client.setRequestHandler(ListRootsRequestSchema, () => + Promise.resolve({ roots: [{ uri: pathToFileURL(directory).href }] }), ) - client.setRequestHandler("roots/list", async () => ({ roots: [{ uri: pathToFileURL(directory).href }] })) return client } @@ -162,7 +154,12 @@ export interface ServerInstructions { } /** An MCP tool in its native shape; consumers adapt it to their own tool format. */ -export type McpTool = McpCatalog.McpTool +export interface McpTool { + /** Shared cached definition; consumers must copy rather than mutate it. */ + readonly def: MCPToolDef + readonly client: MCPClient + readonly timeout?: number +} export interface Interface { readonly status: () => Effect.Effect> @@ -193,11 +190,7 @@ export interface Interface { mcpName: string, onAuthorization?: (authorizationUrl: string) => void, ) => Effect.Effect - readonly finishAuth: ( - mcpName: string, - authorizationCode: string, - iss?: string, - ) => Effect.Effect + readonly finishAuth: (mcpName: string, authorizationCode: string) => Effect.Effect readonly removeAuth: (mcpName: string) => Effect.Effect readonly supportsOAuth: (mcpName: string) => Effect.Effect readonly hasStoredTokens: (mcpName: string) => Effect.Effect @@ -298,18 +291,11 @@ const layer = Layer.effect( Effect.map((client) => ({ client, transportName: name })), Effect.catch((error) => { const lastError = error instanceof Error ? error : new Error(String(error)) - const registrationRejected = - error instanceof RegistrationRejectedError || - lastError.message.includes("registration") || - lastError.message.includes("client_id") const isAuthError = - error instanceof UnauthorizedError || - registrationRejected || - (authProvider && error instanceof SdkHttpError && error.status === 401) || - (authProvider && lastError.message.includes("OAuth")) + error instanceof UnauthorizedError || (authProvider && lastError.message.includes("OAuth")) if (isAuthError) { - if (registrationRejected) { + if (lastError.message.includes("registration") || lastError.message.includes("client_id")) { lastStatus = { status: "needs_client_registration" as const, error: "Server does not support dynamic client registration. Please provide clientId in config.", @@ -468,16 +454,12 @@ const layer = Layer.effect( ) } - client.setNotificationHandler("notifications/message", (notification) => + client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => bridge.promise(serverLog(name, notification.params)), ) if (!client.getServerCapabilities()?.tools) return - client.onToolsChanged = async (error) => { - if (error) { - await bridge.promise(Effect.logWarning("failed to refresh MCP tools", { server: name, error: error.message })) - return - } + client.setNotificationHandler(ToolListChangedNotificationSchema, async () => { if (s.clients[name] !== client || s.status[name]?.status !== "connected") return const listed = await bridge.promise(McpCatalog.defs(client, timeout)) @@ -486,7 +468,7 @@ const layer = Layer.effect( s.defs[name] = listed await bridge.promise(events.publish(ToolsChanged, { server: name }).pipe(Effect.ignore)) - } + }) } function serverLog(name: string, params: LoggingMessageNotification["params"]) { @@ -922,7 +904,7 @@ const layer = Layer.effect( }), ) - const callback = yield* Effect.promise(() => callbackPromise) + const code = yield* Effect.promise(() => callbackPromise) const storedState = yield* auth.getOAuthState(mcpName) if (storedState !== result.oauthState) { @@ -930,20 +912,16 @@ const layer = Layer.effect( throw new Error("OAuth state mismatch - potential CSRF attack") } yield* auth.clearOAuthState(mcpName) - return yield* finishAuth(mcpName, callback.code, callback.iss) + return yield* finishAuth(mcpName, code) }) - const finishAuth = Effect.fn("MCP.finishAuth")(function* ( - mcpName: string, - authorizationCode: string, - iss?: string, - ) { + const finishAuth = Effect.fn("MCP.finishAuth")(function* (mcpName: string, authorizationCode: string) { yield* requireMcpConfig(mcpName) const pending = pendingOAuthTransports.get(mcpName) if (!pending) throw new Error(`No pending OAuth flow for MCP server: ${mcpName}`) const error = yield* Effect.tryPromise({ - try: () => pending.transport.finishAuth(authorizationCode, iss), + try: () => pending.transport.finishAuth(authorizationCode), catch: (error) => error, }).pipe( Effect.match({ diff --git a/packages/opencode/src/mcp/oauth-callback.ts b/packages/opencode/src/mcp/oauth-callback.ts index 71f6ec9539..84007902b8 100644 --- a/packages/opencode/src/mcp/oauth-callback.ts +++ b/packages/opencode/src/mcp/oauth-callback.ts @@ -9,13 +9,8 @@ const OAUTH_CALLBACK_HOST = "127.0.0.1" let currentPort = OAUTH_CALLBACK_PORT let currentPath = OAUTH_CALLBACK_PATH -export interface AuthorizationCallback { - code: string - iss?: string -} - interface PendingAuth { - resolve: (callback: AuthorizationCallback) => void + resolve: (code: string) => void reject: (error: Error) => void timeout: ReturnType } @@ -54,7 +49,6 @@ function handleRequest(req: import("http").IncomingMessage, res: import("http"). } const code = url.searchParams.get("code") - const iss = url.searchParams.get("iss") ?? undefined const state = url.searchParams.get("state") const error = url.searchParams.get("error") const errorDescription = url.searchParams.get("error_description") @@ -101,7 +95,7 @@ function handleRequest(req: import("http").IncomingMessage, res: import("http"). clearTimeout(pending.timeout) pendingAuths.delete(state) cleanupStateIndex(state) - pending.resolve({ code, iss }) + pending.resolve(code) res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }) res.end(OauthCallbackPage.success({ provider: "MCP" })) @@ -136,7 +130,7 @@ export async function ensureRunning(redirectUri?: string): Promise { }) } -export function waitForCallback(oauthState: string, mcpName?: string): Promise { +export function waitForCallback(oauthState: string, mcpName?: string): Promise { if (mcpName) mcpNameToState.set(mcpName, oauthState) return new Promise((resolve, reject) => { const timeout = setTimeout(() => { diff --git a/packages/opencode/src/mcp/oauth-provider.ts b/packages/opencode/src/mcp/oauth-provider.ts index a3f99a55f5..596bfe1d55 100644 --- a/packages/opencode/src/mcp/oauth-provider.ts +++ b/packages/opencode/src/mcp/oauth-provider.ts @@ -1,9 +1,10 @@ +import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js" import type { - OAuthClientProvider, OAuthClientMetadata, - StoredOAuthTokens, - StoredOAuthClientInformation, -} from "@modelcontextprotocol/client" + OAuthTokens, + OAuthClientInformation, + OAuthClientInformationFull, +} from "@modelcontextprotocol/sdk/shared/auth.js" import { Effect } from "effect" import { McpAuth } from "./auth" @@ -22,14 +23,6 @@ export interface McpOAuthCallbacks { onRedirect: (url: URL) => void | Promise } -function registrationMetadata(info: StoredOAuthClientInformation) { - return { - clientIdIssuedAt: "client_id_issued_at" in info ? info.client_id_issued_at : undefined, - clientSecretExpiresAt: "client_secret_expires_at" in info ? info.client_secret_expires_at : undefined, - redirectUris: "redirect_uris" in info ? info.redirect_uris : undefined, - } -} - export class McpOAuthProvider implements OAuthClientProvider { constructor( protected mcpName: string, @@ -59,21 +52,18 @@ export class McpOAuthProvider implements OAuthClientProvider { } } - async clientInformation(): Promise { - const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl)) + async clientInformation(): Promise { if (this.config.clientId) { - const issuer = entry?.clientInfo?.clientId === this.config.clientId ? entry.clientInfo.issuer : undefined return { client_id: this.config.clientId, client_secret: this.config.clientSecret, - ...(issuer !== undefined ? { issuer } : {}), } } // Check stored client info (from dynamic registration) // Use getForUrl to validate credentials are for the current server URL + const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl)) if (entry?.clientInfo) { - if (entry.clientInfo.configPreRegistered) return undefined // Check if client secret has expired if (entry.clientInfo.clientSecretExpiresAt && entry.clientInfo.clientSecretExpiresAt < Date.now() / 1000) { return undefined @@ -81,14 +71,6 @@ export class McpOAuthProvider implements OAuthClientProvider { return { client_id: entry.clientInfo.clientId, client_secret: entry.clientInfo.clientSecret, - ...(entry.clientInfo.clientIdIssuedAt !== undefined - ? { client_id_issued_at: entry.clientInfo.clientIdIssuedAt } - : {}), - ...(entry.clientInfo.clientSecretExpiresAt !== undefined - ? { client_secret_expires_at: entry.clientInfo.clientSecretExpiresAt } - : {}), - redirect_uris: entry.clientInfo.redirectUris ? [...entry.clientInfo.redirectUris] : [this.redirectUrl], - ...(entry.clientInfo.issuer !== undefined ? { issuer: entry.clientInfo.issuer } : {}), } } @@ -96,36 +78,22 @@ export class McpOAuthProvider implements OAuthClientProvider { return undefined } - async saveClientInformation(info: StoredOAuthClientInformation): Promise { - if (this.config.clientId && info.client_id === this.config.clientId) { - await Effect.runPromise( - this.auth.updateClientInfo( - this.mcpName, - { clientId: info.client_id, issuer: info.issuer, configPreRegistered: true }, - this.serverUrl, - ), - ) - return - } - - const metadata = registrationMetadata(info) + async saveClientInformation(info: OAuthClientInformationFull): Promise { await Effect.runPromise( this.auth.updateClientInfo( this.mcpName, { clientId: info.client_id, clientSecret: info.client_secret, - clientIdIssuedAt: metadata.clientIdIssuedAt, - clientSecretExpiresAt: metadata.clientSecretExpiresAt, - redirectUris: metadata.redirectUris ? [...metadata.redirectUris] : [this.redirectUrl], - issuer: info.issuer, + clientIdIssuedAt: info.client_id_issued_at, + clientSecretExpiresAt: info.client_secret_expires_at, }, this.serverUrl, ), ) } - async tokens(): Promise { + async tokens(): Promise { // Use getForUrl to validate tokens are for the current server URL const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl)) if (!entry?.tokens) return undefined @@ -138,20 +106,18 @@ export class McpOAuthProvider implements OAuthClientProvider { ? Math.max(0, Math.floor(entry.tokens.expiresAt - Date.now() / 1000)) : undefined, scope: entry.tokens.scope, - issuer: entry.tokens.issuer, } } - async saveTokens(tokens: StoredOAuthTokens): Promise { + async saveTokens(tokens: OAuthTokens): Promise { await Effect.runPromise( this.auth.updateTokens( this.mcpName, { accessToken: tokens.access_token, refreshToken: tokens.refresh_token, - expiresAt: tokens.expires_in !== undefined ? Date.now() / 1000 + tokens.expires_in : undefined, + expiresAt: tokens.expires_in ? Date.now() / 1000 + tokens.expires_in : undefined, scope: tokens.scope, - issuer: tokens.issuer, }, this.serverUrl, ), @@ -215,10 +181,10 @@ export class McpOAuthProvider implements OAuthClientProvider { } export class McpOAuthPendingProvider extends McpOAuthProvider { - private pendingClientInfo?: StoredOAuthClientInformation - private pendingTokens?: StoredOAuthTokens + private pendingClientInfo?: OAuthClientInformationFull + private pendingTokens?: OAuthTokens - override async clientInformation(): Promise { + override async clientInformation(): Promise { if (!this.config.clientId) return this.pendingClientInfo return { client_id: this.config.clientId, @@ -226,15 +192,15 @@ export class McpOAuthPendingProvider extends McpOAuthProvider { } } - override async saveClientInformation(info: StoredOAuthClientInformation): Promise { + override async saveClientInformation(info: OAuthClientInformationFull): Promise { this.pendingClientInfo = info } - override async tokens(): Promise { + override async tokens(): Promise { return this.pendingTokens } - override async saveTokens(tokens: StoredOAuthTokens): Promise { + override async saveTokens(tokens: OAuthTokens): Promise { this.pendingTokens = tokens } @@ -245,7 +211,6 @@ export class McpOAuthPendingProvider extends McpOAuthProvider { async commit(): Promise { if (!this.pendingTokens) return - const pendingMetadata = this.pendingClientInfo ? registrationMetadata(this.pendingClientInfo) : undefined await Effect.runPromise( this.auth.set( this.mcpName, @@ -253,22 +218,16 @@ export class McpOAuthPendingProvider extends McpOAuthProvider { tokens: { accessToken: this.pendingTokens.access_token, refreshToken: this.pendingTokens.refresh_token, - expiresAt: - this.pendingTokens.expires_in !== undefined - ? Date.now() / 1000 + this.pendingTokens.expires_in - : undefined, + expiresAt: this.pendingTokens.expires_in ? Date.now() / 1000 + this.pendingTokens.expires_in : undefined, scope: this.pendingTokens.scope, - issuer: this.pendingTokens.issuer, }, clientInfo: this.pendingClientInfo && !this.config.clientId ? { clientId: this.pendingClientInfo.client_id, clientSecret: this.pendingClientInfo.client_secret, - clientIdIssuedAt: pendingMetadata?.clientIdIssuedAt, - clientSecretExpiresAt: pendingMetadata?.clientSecretExpiresAt, - redirectUris: pendingMetadata?.redirectUris ? [...pendingMetadata.redirectUris] : [this.redirectUrl], - issuer: this.pendingClientInfo.issuer, + clientIdIssuedAt: this.pendingClientInfo.client_id_issued_at, + clientSecretExpiresAt: this.pendingClientInfo.client_secret_expires_at, } : undefined, }, diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/mcp.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/mcp.ts index ca56d10b4e..a6fb064d73 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/mcp.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/mcp.ts @@ -20,7 +20,6 @@ export const AuthStartResponse = Schema.Struct({ }) export const AuthCallbackPayload = Schema.Struct({ code: Schema.String, - iss: Schema.optional(Schema.String), }) export const AuthRemoveResponse = Schema.Struct({ success: Schema.Literal(true), diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/mcp.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/mcp.ts index 3a367a84e7..cdf0cc1e70 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/mcp.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/mcp.ts @@ -38,7 +38,7 @@ export const mcpHandlers = HttpApiBuilder.group(InstanceHttpApi, "mcp", (handler payload: typeof AuthCallbackPayload.Type }) { return yield* mcp - .finishAuth(ctx.params.name, ctx.payload.code, ctx.payload.iss) + .finishAuth(ctx.params.name, ctx.payload.code) .pipe( Effect.catchTag("MCP.NotFoundError", (error) => Effect.fail( diff --git a/packages/opencode/src/session/tools.ts b/packages/opencode/src/session/tools.ts index d93fb66f2d..0f401c7562 100644 --- a/packages/opencode/src/session/tools.ts +++ b/packages/opencode/src/session/tools.ts @@ -388,7 +388,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { if (flags.experimentalCodeMode) return tools for (const [key, entry] of Object.entries(yield* mcp.tools())) { - const item = McpCatalog.convertTool(entry) + const item = McpCatalog.convertTool(entry.def, entry.client, entry.timeout) const execute = item.execute if (!execute) continue diff --git a/packages/opencode/src/tool/code-mode.ts b/packages/opencode/src/tool/code-mode.ts index a046b4093d..332d4b43f1 100644 --- a/packages/opencode/src/tool/code-mode.ts +++ b/packages/opencode/src/tool/code-mode.ts @@ -1,5 +1,5 @@ import * as Tool from "./tool" -import { type CallToolResult } from "@modelcontextprotocol/client" +import { CallToolResultSchema, type CallToolResult } from "@modelcontextprotocol/sdk/types.js" import { Cause, Effect, Schema } from "effect" import { CodeMode, Tool as SandboxTool, toolError } from "@opencode-ai/codemode" import { MCP } from "@/mcp" @@ -145,7 +145,28 @@ const invokeChildTool = Effect.fn("CodeMode.invokeChildTool")(function* (input: ) const result: CallToolResult = yield* Effect.gen(function* () { yield* input.ctx.ask({ permission: input.entry.key, metadata: {}, patterns: ["*"], always: ["*"] }) - return yield* Effect.promise(() => McpCatalog.callTool(input.entry.tool, input.args, input.ctx.abort)) + // Deliberately mirrors McpCatalog.convertTool's transport call so the MCP service stays free of tool-loop concerns. + return yield* Effect.promise(async () => { + const raw = await input.entry.tool.client.callTool( + { name: input.entry.tool.def.name, arguments: input.args }, + CallToolResultSchema, + { + resetTimeoutOnProgress: true, + signal: input.ctx.abort, + timeout: input.entry.tool.timeout, + // The MCP SDK only sends a progress token when this hook is present, enabling timeout resets. + onprogress: () => {}, + }, + ) + if (raw.isError) + throw new Error( + raw.content + .flatMap((item) => (item.type === "text" ? [item.text] : [])) + .filter((text) => text.trim()) + .join("\n\n") || "MCP tool returned an error", + ) + return raw + }) }).pipe( Effect.withSpan("Tool.execute", { attributes: { diff --git a/packages/opencode/test/fixture/mcp-lifecycle-stdio.ts b/packages/opencode/test/fixture/mcp-lifecycle-stdio.ts index 6260c7e3d4..b01ed921cf 100644 --- a/packages/opencode/test/fixture/mcp-lifecycle-stdio.ts +++ b/packages/opencode/test/fixture/mcp-lifecycle-stdio.ts @@ -1,5 +1,6 @@ -import { Server } from "@modelcontextprotocol/server" -import { StdioServerTransport } from "@modelcontextprotocol/server/stdio" +import { Server } from "@modelcontextprotocol/sdk/server/index.js" +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" +import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" if (process.argv.includes("--hang")) { const pidFile = process.env.MCP_LIFECYCLE_PID_FILE @@ -10,7 +11,7 @@ if (process.argv.includes("--hang")) { const server = new Server({ name: "mcp-lifecycle-stdio", version: "1.0.0" }, { capabilities: { tools: {} } }) -server.setRequestHandler("tools/list", () => +server.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [ { diff --git a/packages/opencode/test/fixture/mcp-session-recovery.ts b/packages/opencode/test/fixture/mcp-session-recovery.ts index 381c187f00..c20fb5aa58 100644 --- a/packages/opencode/test/fixture/mcp-session-recovery.ts +++ b/packages/opencode/test/fixture/mcp-session-recovery.ts @@ -1,11 +1,10 @@ -import { Client, LATEST_PROTOCOL_VERSION, StreamableHTTPClientTransport } from "@modelcontextprotocol/client" +import { Client } from "@modelcontextprotocol/sdk/client/index.js" +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" +import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js" const posts: Array<{ method: string; session: string | null }> = [] -const concurrent = process.env.MCP_RECOVERY_CONCURRENT === "1" let initializeCount = 0 let pingCount = 0 -let replacementStarted!: () => void -const replacement = new Promise((resolve) => (replacementStarted = resolve)) const server = Bun.serve({ port: 0, async fetch(request) { @@ -18,7 +17,6 @@ const server = Bun.serve({ if (message.method === "initialize") { initializeCount++ - if (initializeCount === 2) replacementStarted() return Response.json( { jsonrpc: "2.0", @@ -36,8 +34,7 @@ const server = Bun.serve({ if (message.method === "notifications/initialized") return new Response(null, { status: 202 }) pingCount++ - if (concurrent && pingCount === 2) await replacement - if (pingCount <= (concurrent ? 2 : 1)) return new Response("Session not found", { status: 404 }) + if (pingCount === 1) return new Response("Session not found", { status: 404 }) return Response.json({ jsonrpc: "2.0", id: message.id, result: {} }) }, }) @@ -45,8 +42,7 @@ const client = new Client({ name: "test", version: "1" }) try { await client.connect(new StreamableHTTPClientTransport(server.url)) - if (concurrent) await Promise.all([client.ping(), client.ping()]) - else await client.ping() + await client.ping() process.stdout.write(JSON.stringify(posts)) } finally { await client.close() diff --git a/packages/opencode/test/mcp/catalog.test.ts b/packages/opencode/test/mcp/catalog.test.ts index 3d77cb22b7..7b0d6403bb 100644 --- a/packages/opencode/test/mcp/catalog.test.ts +++ b/packages/opencode/test/mcp/catalog.test.ts @@ -1,6 +1,8 @@ import { describe, expect, test } from "bun:test" -import { Client, InMemoryTransport } from "@modelcontextprotocol/client" -import { Server } from "@modelcontextprotocol/server" +import { Client } from "@modelcontextprotocol/sdk/client/index.js" +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js" +import { Server } from "@modelcontextprotocol/sdk/server/index.js" +import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" import { McpCatalog } from "@/mcp/catalog" import { Effect } from "effect" @@ -28,10 +30,7 @@ describe("McpCatalog.convertTool", () => { test("preserves content when structuredContent is also present", async () => { const content = [{ type: "image" as const, mimeType: "image/png", data: "AAAA" }] const structuredContent = { image: { mimeType: "image/png", data: "AAAA" } } - const converted = McpCatalog.convertTool({ - def: mcpTool(), - client: clientReturning({ content, structuredContent }), - }) + const converted = McpCatalog.convertTool(mcpTool(), clientReturning({ content, structuredContent })) const output = await converted.execute?.({}, options) @@ -40,10 +39,7 @@ describe("McpCatalog.convertTool", () => { test("falls back to structuredContent only when content is absent", async () => { const structuredContent = { results: [{ title: "one" }] } - const converted = McpCatalog.convertTool({ - def: mcpTool(), - client: clientReturning({ content: [], structuredContent }), - }) + const converted = McpCatalog.convertTool(mcpTool(), clientReturning({ content: [], structuredContent })) const output = await converted.execute?.({}, options) @@ -54,52 +50,18 @@ describe("McpCatalog.convertTool", () => { }) }) -describe("McpCatalog.callTool", () => { - test("forwards the request options", async () => { - const controller = new AbortController() - let request: unknown - let options: unknown - const client = { - callTool: async (input: unknown, config: unknown) => { - request = input - options = config - return { content: [] } - }, - } as unknown as Client - - await McpCatalog.callTool({ def: mcpTool(), client, timeout: 123 }, { value: true }, controller.signal) - - expect(request).toEqual({ name: "screenshot", arguments: { value: true } }) - expect(options).toMatchObject({ resetTimeoutOnProgress: true, signal: controller.signal, timeout: 123 }) - expect(typeof (options as { onprogress?: unknown }).onprogress).toBe("function") - }) - - test("throws text returned by an MCP tool error", async () => { - const client = clientReturning({ - isError: true, - content: [ - { type: "image", data: "AAAA", mimeType: "image/png" }, - { type: "text", text: "first" }, - { type: "text", text: "second" }, - ], - }) - - await expect(McpCatalog.callTool({ def: mcpTool(), client }, {})).rejects.toThrow("first\n\nsecond") - }) -}) - test("preserves output schema validation across paginated tool discovery", async () => { const server = new Server({ name: "pagination", version: "1.0.0" }, { capabilities: { tools: {} } }) - server.setRequestHandler("tools/list", ({ params }) => + server.setRequestHandler(ListToolsRequestSchema, ({ params }) => Promise.resolve( params?.cursor === "page-2" ? { tools: [ { name: "second", - inputSchema: { type: "object" as const }, + inputSchema: { type: "object" }, outputSchema: { - type: "object" as const, + type: "object", properties: { value: { type: "number" } }, required: ["value"], }, @@ -110,9 +72,9 @@ test("preserves output schema validation across paginated tool discovery", async tools: [ { name: "first", - inputSchema: { type: "object" as const }, + inputSchema: { type: "object" }, outputSchema: { - type: "object" as const, + type: "object", properties: { value: { type: "string" } }, required: ["value"], }, @@ -122,7 +84,7 @@ test("preserves output schema validation across paginated tool discovery", async }, ), ) - server.setRequestHandler("tools/call", ({ params }) => + server.setRequestHandler(CallToolRequestSchema, ({ params }) => Promise.resolve({ content: [], structuredContent: { value: params.name === "first" ? 42 : 1 }, @@ -136,7 +98,9 @@ test("preserves output schema validation across paginated tool discovery", async try { const tools = await Effect.runPromise(McpCatalog.defs(client)) expect(tools?.map((tool) => tool.name)).toEqual(["first", "second"]) - await expect(client.callTool({ name: "first", arguments: {} })).rejects.toThrow(/output schema/i) + await expect(client.callTool({ name: "first", arguments: {} })).rejects.toThrow( + "Structured content does not match the tool's output schema", + ) } finally { await Promise.all([client.close(), server.close()]) } diff --git a/packages/opencode/test/mcp/headers.test.ts b/packages/opencode/test/mcp/headers.test.ts index 323aea2e47..31cfc20d51 100644 --- a/packages/opencode/test/mcp/headers.test.ts +++ b/packages/opencode/test/mcp/headers.test.ts @@ -1,5 +1,7 @@ import { describe, expect } from "bun:test" -import { Server, WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server" +import { Server } from "@modelcontextprotocol/sdk/server/index.js" +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js" +import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Effect } from "effect" import { testEffect } from "../lib/effect" @@ -11,7 +13,7 @@ const serve = Effect.acquireRelease( Effect.promise(async () => { const requests: Headers[] = [] const protocol = new Server({ name: "headers", version: "1.0.0" }, { capabilities: { tools: {} } }) - protocol.setRequestHandler("tools/list", () => Promise.resolve({ tools: [] })) + protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] })) const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID(), enableJsonResponse: true, @@ -36,11 +38,6 @@ const serve = Effect.acquireRelease( (server) => Effect.promise(server.close), ) -const serveUnauthorized = Effect.acquireRelease( - Effect.sync(() => Bun.serve({ port: 0, fetch: () => new Response("Unauthorized", { status: 401 }) })), - (server) => Effect.sync(() => server.stop(true)), -) - describe("mcp.headers", () => { it.instance("headers are passed to transports when oauth is enabled (default)", () => Effect.gen(function* () { @@ -102,18 +99,4 @@ describe("mcp.headers", () => { } }), ) - - it.instance("reports 401 as failed when oauth is explicitly disabled", () => - Effect.gen(function* () { - const server = yield* serveUnauthorized - const mcp = yield* MCP.Service - const result = yield* mcp.add("unauthorized-no-oauth", { - type: "remote", - url: server.url.toString(), - oauth: false, - }) - - expect(result.status).toMatchObject({ "unauthorized-no-oauth": { status: "failed" } }) - }), - ) }) diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index b001898779..80c8fd22f8 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -1,12 +1,18 @@ import path from "node:path" import { pathToFileURL } from "node:url" import { expect } from "bun:test" +import { Server } from "@modelcontextprotocol/sdk/server/index.js" +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js" import { - Server, - WebStandardStreamableHTTPServerTransport, + GetPromptRequestSchema, + ListPromptsRequestSchema, + ListResourcesRequestSchema, + ListResourceTemplatesRequestSchema, + ListToolsRequestSchema, + ReadResourceRequestSchema, type ServerCapabilities, type Tool, -} from "@modelcontextprotocol/server" +} from "@modelcontextprotocol/sdk/types.js" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Cause, Effect, Exit } from "effect" import type { MCP as MCPNS } from "../../src/mcp/index" @@ -60,35 +66,35 @@ function lifecycleServer(input?: { capabilities?: ServerCapabilities; instructio }) if (capabilities.tools) { - protocol.setRequestHandler("tools/list", (request) => { + protocol.setRequestHandler(ListToolsRequestSchema, (request) => { if (state.listToolsError) throw new Error(state.listToolsError) const page = state.toolPages?.[request.params?.cursor ?? "initial"] return Promise.resolve({ tools: page?.items ?? state.tools, nextCursor: page?.nextCursor }) }) } if (capabilities.prompts) { - protocol.setRequestHandler("prompts/list", (request) => { + protocol.setRequestHandler(ListPromptsRequestSchema, (request) => { const page = state.promptPages?.[request.params?.cursor ?? "initial"] return Promise.resolve({ prompts: page?.items ?? state.prompts, nextCursor: page?.nextCursor }) }) - protocol.setRequestHandler("prompts/get", async () => { + protocol.setRequestHandler(GetPromptRequestSchema, async () => { if (state.requestDelay) await Bun.sleep(state.requestDelay) return { messages: [{ role: "user", content: { type: "text", text: "prompt result" } }] } }) } if (capabilities.resources) { - protocol.setRequestHandler("resources/list", (request) => { + protocol.setRequestHandler(ListResourcesRequestSchema, (request) => { const page = state.resourcePages?.[request.params?.cursor ?? "initial"] return Promise.resolve({ resources: page?.items ?? state.resources, nextCursor: page?.nextCursor }) }) - protocol.setRequestHandler("resources/templates/list", (request) => { + protocol.setRequestHandler(ListResourceTemplatesRequestSchema, (request) => { const page = state.resourceTemplatePages?.[request.params?.cursor ?? "initial"] return Promise.resolve({ resourceTemplates: page?.items ?? state.resourceTemplates, nextCursor: page?.nextCursor, }) }) - protocol.setRequestHandler("resources/read", async (request) => { + protocol.setRequestHandler(ReadResourceRequestSchema, async (request) => { if (state.requestDelay) await Bun.sleep(state.requestDelay) return { contents: [{ uri: request.params.uri, text: "resource result" }] } }) @@ -139,7 +145,7 @@ function hangingLifecycleServer() { return Effect.acquireRelease( Effect.promise(async () => { const protocol = new Server({ name: "mcp-lifecycle-hanging", version: "1.0.0" }, { capabilities: { tools: {} } }) - protocol.setRequestHandler("tools/list", () => Promise.resolve({ tools: [] })) + protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] })) const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID(), enableJsonResponse: true, @@ -278,7 +284,7 @@ it.instance("follows cursors when listing tools, prompts, resources, and templat }), ) -it.instance("accepts empty cursors and terminates on repeated cursors", () => +it.instance("accepts empty cursors and rejects repeated cursors", () => Effect.gen(function* () { const empty = yield* lifecycleServer({ capabilities: { prompts: {} } }) empty.state.promptPages = { @@ -295,8 +301,7 @@ it.instance("accepts empty cursors and terminates on repeated cursors", () => const result = yield* mcp.add("looping-cursor", remote(looping.url)) expect(Object.keys(yield* mcp.prompts())).toEqual(["empty-cursor:prompt-one", "empty-cursor:prompt-two"]) - expect(statusName(result.status, "looping-cursor")).toBe("connected") - expect(Object.keys(yield* mcp.tools())).toEqual([]) + expect(statusName(result.status, "looping-cursor")).toBe("failed") }), ) diff --git a/packages/opencode/test/mcp/oauth-auto-connect.test.ts b/packages/opencode/test/mcp/oauth-auto-connect.test.ts index 735dfaf8be..5f8889068c 100644 --- a/packages/opencode/test/mcp/oauth-auto-connect.test.ts +++ b/packages/opencode/test/mcp/oauth-auto-connect.test.ts @@ -1,5 +1,7 @@ import { expect } from "bun:test" -import { Server, WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server" +import { Server } from "@modelcontextprotocol/sdk/server/index.js" +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js" +import { ListResourcesRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { FSUtil } from "@opencode-ai/core/fs-util" @@ -38,13 +40,13 @@ function serveOAuthMcp(options: OAuthMcpOptions = {}) { let requiresAuth = true if (capabilities === "tools") { - protocol.setRequestHandler("tools/list", () => { + protocol.setRequestHandler(ListToolsRequestSchema, () => { listToolsCalls++ return Promise.resolve({ tools: [{ name: "test_tool", inputSchema: { type: "object" } }] }) }) } if (capabilities === "resources") { - protocol.setRequestHandler("resources/list", () => + protocol.setRequestHandler(ListResourcesRequestSchema, () => Promise.resolve({ resources: [{ name: "docs", uri: "docs://readme" }] }), ) } diff --git a/packages/opencode/test/mcp/oauth-browser.test.ts b/packages/opencode/test/mcp/oauth-browser.test.ts index 507b59b691..9573805a9a 100644 --- a/packages/opencode/test/mcp/oauth-browser.test.ts +++ b/packages/opencode/test/mcp/oauth-browser.test.ts @@ -1,5 +1,7 @@ import { expect } from "bun:test" -import { Server, WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server" +import { Server } from "@modelcontextprotocol/sdk/server/index.js" +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js" +import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Deferred, Effect, Layer, Option } from "effect" import { Config } from "../../src/config/config" @@ -39,7 +41,7 @@ const serveOAuthMcp = Effect.acquireRelease( Effect.promise(async () => { const requests: Array<{ pathname: string; headers: Headers }> = [] const protocol = new Server({ name: "oauth-browser", version: "1.0.0" }, { capabilities: { tools: {} } }) - protocol.setRequestHandler("tools/list", () => Promise.resolve({ tools: [] })) + protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] })) const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID(), enableJsonResponse: true, diff --git a/packages/opencode/test/mcp/oauth-callback.test.ts b/packages/opencode/test/mcp/oauth-callback.test.ts index b7db18034f..1666a37142 100644 --- a/packages/opencode/test/mcp/oauth-callback.test.ts +++ b/packages/opencode/test/mcp/oauth-callback.test.ts @@ -74,7 +74,7 @@ describe("McpOAuthCallback.ensureRunning", () => { const response = await fetch(`${redirectUri}?code=code&state=success`) expect(response.status).toBe(200) - expect(await callback).toEqual({ code: "code", iss: undefined }) + expect(await callback).toBe("code") expect(McpOAuthCallback.isRunning()).toBe(false) }) diff --git a/packages/opencode/test/mcp/oauth-provider.test.ts b/packages/opencode/test/mcp/oauth-provider.test.ts index 64c2cb6687..249c49e8f9 100644 --- a/packages/opencode/test/mcp/oauth-provider.test.ts +++ b/packages/opencode/test/mcp/oauth-provider.test.ts @@ -1,4 +1,5 @@ import { test, expect, describe } from "bun:test" +import { determineScope } from "@modelcontextprotocol/sdk/client/auth.js" import { McpOAuthProvider, OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "../../src/mcp/oauth-provider" import type { McpAuth } from "../../src/mcp/auth" @@ -59,3 +60,43 @@ describe("McpOAuthProvider.clientMetadata", () => { expect(provider.clientMetadata.token_endpoint_auth_method).toBe("none") }) }) + +describe("MCP OAuth scope selection", () => { + test("adds offline_access when the authorization server and client support refresh tokens", () => { + expect( + determineScope({ + resourceMetadata: { + resource: "https://mcp.example.com/mcp", + scopes_supported: ["resource.read"], + }, + authServerMetadata: { + issuer: "https://auth.example.com", + authorization_endpoint: "https://auth.example.com/authorize", + token_endpoint: "https://auth.example.com/token", + response_types_supported: ["code"], + scopes_supported: ["resource.read", "offline_access"], + }, + clientMetadata: makeProvider({}).clientMetadata, + }), + ).toBe("resource.read offline_access") + }) + + test("does not add unsupported authorization server scopes", () => { + expect( + determineScope({ + resourceMetadata: { + resource: "https://mcp.example.com/mcp", + scopes_supported: ["resource.read"], + }, + authServerMetadata: { + issuer: "https://auth.example.com", + authorization_endpoint: "https://auth.example.com/authorize", + token_endpoint: "https://auth.example.com/token", + response_types_supported: ["code"], + scopes_supported: ["resource.read"], + }, + clientMetadata: makeProvider({}).clientMetadata, + }), + ).toBe("resource.read") + }) +}) diff --git a/packages/opencode/test/mcp/session-recovery.test.ts b/packages/opencode/test/mcp/session-recovery.test.ts index f7c5787a4e..6586508220 100644 --- a/packages/opencode/test/mcp/session-recovery.test.ts +++ b/packages/opencode/test/mcp/session-recovery.test.ts @@ -24,24 +24,4 @@ describe("mcp session recovery", () => { { method: "ping", session: "replacement" }, ]) }) - - test("retries a concurrent stale response after recovery completes", async () => { - const child = Bun.spawn([process.execPath, path.join(import.meta.dir, "../fixture/mcp-session-recovery.ts")], { - cwd: path.join(import.meta.dir, "../.."), - env: { ...process.env, MCP_RECOVERY_CONCURRENT: "1" }, - stdout: "pipe", - stderr: "pipe", - }) - const [code, stdout, stderr] = await Promise.all([ - child.exited, - Bun.readableStreamToText(child.stdout), - Bun.readableStreamToText(child.stderr), - ]) - - expect(code, stderr).toBe(0) - const posts = JSON.parse(stdout) as Array<{ method: string; session: string | null }> - expect(posts.filter((post) => post.method === "initialize").map((post) => post.session)).toEqual([null, null]) - expect(posts.filter((post) => post.method === "ping" && post.session === "expired")).toHaveLength(2) - expect(posts.filter((post) => post.method === "ping" && post.session === "replacement")).toHaveLength(2) - }) }) diff --git a/packages/opencode/test/tool/code-mode-integration.test.ts b/packages/opencode/test/tool/code-mode-integration.test.ts index 32cb420681..671acd8962 100644 --- a/packages/opencode/test/tool/code-mode-integration.test.ts +++ b/packages/opencode/test/tool/code-mode-integration.test.ts @@ -8,14 +8,15 @@ import { Session } from "@/session/session" import { Tool } from "@/tool/tool" import * as Truncate from "@/tool/truncate" import { MessageID, SessionID } from "@/session/schema" -import { Server } from "@modelcontextprotocol/server" +import { Server } from "@modelcontextprotocol/sdk/server/index.js" +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js" +import type { Client } from "@modelcontextprotocol/sdk/client/index.js" import { - InMemoryTransport, + CallToolRequestSchema, LATEST_PROTOCOL_VERSION, - type CallToolResult, - type Client, + ListToolsRequestSchema, type Tool as MCPToolDef, -} from "@modelcontextprotocol/client" +} from "@modelcontextprotocol/sdk/types.js" import { Cause, Effect, Exit, Layer } from "effect" const PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" @@ -99,7 +100,7 @@ const TOOL_DEFS: MCPToolDef[] = [ }, ] as MCPToolDef[] -function handleCall(name: string, args: Record): CallToolResult { +function handleCall(name: string, args: Record) { switch (name) { case "get_text": return { content: [{ type: "text", text: `hello ${args.name}` }] } @@ -121,8 +122,8 @@ let description: string async function buildTool() { const server = new Server({ name: SERVER, version: "1.0.0" }, { capabilities: { tools: {} } }) - server.setRequestHandler("tools/list", async () => ({ tools: TOOL_DEFS })) - server.setRequestHandler("tools/call", async (req) => + server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_DEFS })) + server.setRequestHandler(CallToolRequestSchema, async (req) => handleCall(req.params.name, (req.params.arguments ?? {}) as Record), ) diff --git a/packages/opencode/test/tool/code-mode.test.ts b/packages/opencode/test/tool/code-mode.test.ts index cc32d2a5f3..34b3faa610 100644 --- a/packages/opencode/test/tool/code-mode.test.ts +++ b/packages/opencode/test/tool/code-mode.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { CODE_MODE_TOOL, CodeModeTool, Parameters, describeCatalog } from "@/tool/code-mode" -import type { Tool as MCPToolDef } from "@modelcontextprotocol/client" +import type { Tool as MCPToolDef } from "@modelcontextprotocol/sdk/types.js" import type { PermissionV1 } from "@opencode-ai/core/v1/permission" import { Agent } from "@/agent/agent" import { MCP } from "@/mcp" diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index c0810a6d7e..c8c5fac595 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -20,7 +20,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" import { MCP } from "@/mcp" -import type { Tool as MCPToolDef } from "@modelcontextprotocol/client" +import type { Tool as MCPToolDef } from "@modelcontextprotocol/sdk/types.js" const configLayer = TestConfig.layer({ directories: () => InstanceState.directory.pipe(Effect.map((dir) => [path.join(dir, ".opencode")])), diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index babdbc9c51..9ed0084aac 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -2328,7 +2328,6 @@ export class Auth2 extends HeyApiClient { directory?: string workspace?: string code?: string - iss?: string }, options?: Options, ) { @@ -2341,7 +2340,6 @@ export class Auth2 extends HeyApiClient { { in: "query", key: "directory" }, { in: "query", key: "workspace" }, { in: "body", key: "code" }, - { in: "body", key: "iss" }, ], }, ], diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index f0db3236ea..42d224780d 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -8563,7 +8563,6 @@ export type McpAuthStartResponse = McpAuthStartResponses[keyof McpAuthStartRespo export type McpAuthCallbackData = { body?: { code: string - iss?: string } path: { name: string diff --git a/patches/@modelcontextprotocol%2Fclient@2.0.0.patch b/patches/@modelcontextprotocol%2Fclient@2.0.0.patch deleted file mode 100644 index 833adea8a9..0000000000 --- a/patches/@modelcontextprotocol%2Fclient@2.0.0.patch +++ /dev/null @@ -1,214 +0,0 @@ -diff --git a/dist/index.cjs b/dist/index.cjs -index 635f1c0..9214f0b 100644 ---- a/dist/index.cjs -+++ b/dist/index.cjs -@@ -3211,6 +3211,7 @@ var Client = class extends require_src.Protocol { - */ - async _connectPlainLegacy(transport, options) { - await super.connect(transport); -+ transport.onsessionexpired = () => this._legacyHandshake(transport, options); - if (transport.sessionId !== void 0) { - const negotiatedProtocolVersion = this._negotiatedProtocolVersion; - if (negotiatedProtocolVersion !== void 0) transport.setProtocolVersion?.(negotiatedProtocolVersion); -@@ -3227,6 +3228,7 @@ var Client = class extends require_src.Protocol { - * the handshake; its completion sets the negotiated (legacy) version. - */ - async _legacyHandshake(transport, options) { -+ transport.onsessionexpired = () => this._legacyHandshake(transport, options); - const legacyVersions = require_src.legacyProtocolVersions(this._supportedProtocolVersions); - try { - const offeredVersion = legacyVersions[0]; -@@ -3265,6 +3267,7 @@ var Client = class extends require_src.Protocol { - await super.connect(transport); - const negotiatedProtocolVersion = this._negotiatedProtocolVersion; - if (negotiatedProtocolVersion !== void 0 && transport.setProtocolVersion) transport.setProtocolVersion(negotiatedProtocolVersion); -+ if (negotiatedProtocolVersion !== void 0 && !require_src.isModernProtocolVersion(negotiatedProtocolVersion)) transport.onsessionexpired = () => this._legacyHandshake(transport, options); - return; - } - this._resetConnectionState(); -@@ -5294,10 +5297,32 @@ var StreamableHTTPClientTransport = class { - } - } - async send(message, options) { -- return this._send(message, options, false); -+ return this._send(message, options, false, 0, false); -+ } -+ async _recoverSession(expiredSessionId) { -+ if (this._sessionRecovery) return this._sessionRecovery; -+ if (!this.onsessionexpired) return false; -+ if (this._sessionId !== expiredSessionId) return true; -+ this._sessionId = void 0; -+ this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired()).then(() => true); -+ try { -+ return await this._sessionRecovery; -+ } catch (error) { -+ this._sessionId = void 0; -+ await this.close(); -+ throw error; -+ } finally { -+ this._sessionRecovery = void 0; -+ } - } -- async _send(message, options, isAuthRetry, stepUpRetries = 0) { -+ async _send(message, options, isAuthRetry, stepUpRetries = 0, isSessionRetry = false) { - try { -+ const isHandshake = Array.isArray(message) ? message.some((m) => require_src.isInitializeRequest(m)) : require_src.isInitializeRequest(message); -+ const isInitialized = Array.isArray(message) ? message.some((m) => require_src.isInitializedNotification(m)) : require_src.isInitializedNotification(message); -+ if (this._sessionRecovery && !isHandshake && !isInitialized) { -+ await this._sessionRecovery; -+ options?.requestSignal?.throwIfAborted(); -+ } - const { resumptionToken, onresumptiontoken } = options || {}; - if (resumptionToken) { - this._startOrAuthSse({ -@@ -5309,8 +5334,8 @@ var StreamableHTTPClientTransport = class { - } - const headers = await this._commonHeaders(); - this._applyBodyDerivedHeaders(headers, message); -- const isHandshake = Array.isArray(message) ? message.some((m) => require_src.isInitializeRequest(m)) : require_src.isInitializeRequest(message); - if (isHandshake) headers.delete("mcp-session-id"); -+ const requestSessionId = headers.get("mcp-session-id") || void 0; - if (options?.headers !== void 0) for (const [name, value] of Object.entries(options.headers)) { - if (RESERVED_REQUEST_HEADER_NAMES.has(name.toLowerCase())) continue; - headers.set(name, value); -@@ -5332,8 +5357,14 @@ var StreamableHTTPClientTransport = class { - signal - }; - const response = await (this._fetch ?? fetch)(this._url, init); -- if (isHandshake && response.ok) this._sessionId = response.headers.get("mcp-session-id") || void 0; -+ if (isHandshake && response.ok && (requestSessionId === void 0 || this._sessionId === requestSessionId)) this._sessionId = response.headers.get("mcp-session-id") || void 0; - if (!response.ok) { -+ if (response.status === 404 && requestSessionId && !isSessionRetry && !isInitialized) { -+ if (await this._recoverSession(requestSessionId)) { -+ options?.requestSignal?.throwIfAborted(); -+ return this._send(message, options, isAuthRetry, stepUpRetries, true); -+ } -+ } - if (response.status === 401 && this._authProvider) { - if (response.headers.has("www-authenticate")) { - const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response); -@@ -5351,7 +5382,7 @@ var StreamableHTTPClientTransport = class { - throw markAuthSeamEscape(error); - } - await response.text?.().catch(() => {}); -- return this._send(message, options, true, stepUpRetries); -+ return this._send(message, options, true, stepUpRetries, isSessionRetry); - } - await response.text?.().catch(() => {}); - if (isAuthRetry) throw markAuthSeamEscape(new require_src.SdkHttpError(require_src.SdkErrorCode.ClientHttpAuthentication, "Server returned 401 after re-authentication", { -@@ -5371,7 +5402,7 @@ var StreamableHTTPClientTransport = class { - statusText: response.statusText, - text - }, stepUpRetries) !== "AUTHORIZED") throw markAuthSeamEscape(new UnauthorizedError()); -- return this._send(message, options, isAuthRetry, stepUpRetries + 1); -+ return this._send(message, options, isAuthRetry, stepUpRetries + 1, isSessionRetry); - } - } - if (response.status === 400 && typeof text === "string" && this._isModernEnvelopedRequest(message)) try { -diff --git a/dist/index.mjs b/dist/index.mjs -index f02ce3c..0a5a649 100644 ---- a/dist/index.mjs -+++ b/dist/index.mjs -@@ -3208,6 +3208,7 @@ var Client = class extends Protocol { - */ - async _connectPlainLegacy(transport, options) { - await super.connect(transport); -+ transport.onsessionexpired = () => this._legacyHandshake(transport, options); - if (transport.sessionId !== void 0) { - const negotiatedProtocolVersion = this._negotiatedProtocolVersion; - if (negotiatedProtocolVersion !== void 0) transport.setProtocolVersion?.(negotiatedProtocolVersion); -@@ -3224,6 +3225,7 @@ var Client = class extends Protocol { - * the handshake; its completion sets the negotiated (legacy) version. - */ - async _legacyHandshake(transport, options) { -+ transport.onsessionexpired = () => this._legacyHandshake(transport, options); - const legacyVersions = legacyProtocolVersions(this._supportedProtocolVersions); - try { - const offeredVersion = legacyVersions[0]; -@@ -3262,6 +3264,7 @@ var Client = class extends Protocol { - await super.connect(transport); - const negotiatedProtocolVersion = this._negotiatedProtocolVersion; - if (negotiatedProtocolVersion !== void 0 && transport.setProtocolVersion) transport.setProtocolVersion(negotiatedProtocolVersion); -+ if (negotiatedProtocolVersion !== void 0 && !isModernProtocolVersion(negotiatedProtocolVersion)) transport.onsessionexpired = () => this._legacyHandshake(transport, options); - return; - } - this._resetConnectionState(); -@@ -5291,10 +5294,32 @@ var StreamableHTTPClientTransport = class { - } - } - async send(message, options) { -- return this._send(message, options, false); -+ return this._send(message, options, false, 0, false); -+ } -+ async _recoverSession(expiredSessionId) { -+ if (this._sessionRecovery) return this._sessionRecovery; -+ if (!this.onsessionexpired) return false; -+ if (this._sessionId !== expiredSessionId) return true; -+ this._sessionId = void 0; -+ this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired()).then(() => true); -+ try { -+ return await this._sessionRecovery; -+ } catch (error) { -+ this._sessionId = void 0; -+ await this.close(); -+ throw error; -+ } finally { -+ this._sessionRecovery = void 0; -+ } - } -- async _send(message, options, isAuthRetry, stepUpRetries = 0) { -+ async _send(message, options, isAuthRetry, stepUpRetries = 0, isSessionRetry = false) { - try { -+ const isHandshake = Array.isArray(message) ? message.some((m) => isInitializeRequest(m)) : isInitializeRequest(message); -+ const isInitialized = Array.isArray(message) ? message.some((m) => isInitializedNotification(m)) : isInitializedNotification(message); -+ if (this._sessionRecovery && !isHandshake && !isInitialized) { -+ await this._sessionRecovery; -+ options?.requestSignal?.throwIfAborted(); -+ } - const { resumptionToken, onresumptiontoken } = options || {}; - if (resumptionToken) { - this._startOrAuthSse({ -@@ -5306,8 +5331,8 @@ var StreamableHTTPClientTransport = class { - } - const headers = await this._commonHeaders(); - this._applyBodyDerivedHeaders(headers, message); -- const isHandshake = Array.isArray(message) ? message.some((m) => isInitializeRequest(m)) : isInitializeRequest(message); - if (isHandshake) headers.delete("mcp-session-id"); -+ const requestSessionId = headers.get("mcp-session-id") || void 0; - if (options?.headers !== void 0) for (const [name, value] of Object.entries(options.headers)) { - if (RESERVED_REQUEST_HEADER_NAMES.has(name.toLowerCase())) continue; - headers.set(name, value); -@@ -5329,8 +5354,14 @@ var StreamableHTTPClientTransport = class { - signal - }; - const response = await (this._fetch ?? fetch)(this._url, init); -- if (isHandshake && response.ok) this._sessionId = response.headers.get("mcp-session-id") || void 0; -+ if (isHandshake && response.ok && (requestSessionId === void 0 || this._sessionId === requestSessionId)) this._sessionId = response.headers.get("mcp-session-id") || void 0; - if (!response.ok) { -+ if (response.status === 404 && requestSessionId && !isSessionRetry && !isInitialized) { -+ if (await this._recoverSession(requestSessionId)) { -+ options?.requestSignal?.throwIfAborted(); -+ return this._send(message, options, isAuthRetry, stepUpRetries, true); -+ } -+ } - if (response.status === 401 && this._authProvider) { - if (response.headers.has("www-authenticate")) { - const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response); -@@ -5348,7 +5379,7 @@ var StreamableHTTPClientTransport = class { - throw markAuthSeamEscape(error); - } - await response.text?.().catch(() => {}); -- return this._send(message, options, true, stepUpRetries); -+ return this._send(message, options, true, stepUpRetries, isSessionRetry); - } - await response.text?.().catch(() => {}); - if (isAuthRetry) throw markAuthSeamEscape(new SdkHttpError(SdkErrorCode.ClientHttpAuthentication, "Server returned 401 after re-authentication", { -@@ -5368,7 +5399,7 @@ var StreamableHTTPClientTransport = class { - statusText: response.statusText, - text - }, stepUpRetries) !== "AUTHORIZED") throw markAuthSeamEscape(new UnauthorizedError()); -- return this._send(message, options, isAuthRetry, stepUpRetries + 1); -+ return this._send(message, options, isAuthRetry, stepUpRetries + 1, isSessionRetry); - } - } - if (response.status === 400 && typeof text === "string" && this._isModernEnvelopedRequest(message)) try { diff --git a/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch b/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch new file mode 100644 index 0000000000..13b8000a01 --- /dev/null +++ b/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch @@ -0,0 +1,629 @@ +diff --git a/dist/cjs/client/index.d.ts b/dist/cjs/client/index.d.ts +index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644 +--- a/dist/cjs/client/index.d.ts ++++ b/dist/cjs/client/index.d.ts +@@ -428,6 +428,8 @@ export declare class Client>; ++ callTool(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise>; + callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{ + [x: string]: unknown; + content: ({ +diff --git a/dist/esm/client/index.d.ts b/dist/esm/client/index.d.ts +index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644 +--- a/dist/esm/client/index.d.ts ++++ b/dist/esm/client/index.d.ts +@@ -428,6 +428,8 @@ export declare class Client>; ++ callTool(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise>; + callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{ + [x: string]: unknown; + content: ({ +diff --git a/dist/cjs/client/index.js b/dist/cjs/client/index.js +index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c101584c84 100644 +--- a/dist/cjs/client/index.js ++++ b/dist/cjs/client/index.js +@@ -288,41 +288,16 @@ class Client extends protocol_js_1.Protocol { + } + async connect(transport, options) { + await super.connect(transport); ++ transport.onsessionexpired = async () => { ++ await this._initialize(transport); ++ }; + // When transport sessionId is already set this means we are trying to reconnect. + // In this case we don't need to initialize again. + if (transport.sessionId !== undefined) { + return; + } + try { +- const result = await this.request({ +- method: 'initialize', +- params: { +- protocolVersion: types_js_1.LATEST_PROTOCOL_VERSION, +- capabilities: this._capabilities, +- clientInfo: this._clientInfo +- } +- }, types_js_1.InitializeResultSchema, options); +- if (result === undefined) { +- throw new Error(`Server sent invalid initialize result: ${result}`); +- } +- if (!types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) { +- throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`); +- } +- this._serverCapabilities = result.capabilities; +- this._serverVersion = result.serverInfo; +- // HTTP transports must set the protocol version in each header after initialization. +- if (transport.setProtocolVersion) { +- transport.setProtocolVersion(result.protocolVersion); +- } +- this._instructions = result.instructions; +- await this.notification({ +- method: 'notifications/initialized' +- }); +- // Set up list changed handlers now that we know server capabilities +- if (this._pendingListChangedConfig) { +- this._setupListChangedHandlers(this._pendingListChangedConfig); +- this._pendingListChangedConfig = undefined; +- } ++ await this._initialize(transport, options); + } + catch (error) { + // Disconnect if initialization fails. +@@ -330,6 +305,37 @@ class Client extends protocol_js_1.Protocol { + throw error; + } + } ++ async _initialize(transport, options) { ++ const result = await this.request({ ++ method: 'initialize', ++ params: { ++ protocolVersion: types_js_1.LATEST_PROTOCOL_VERSION, ++ capabilities: this._capabilities, ++ clientInfo: this._clientInfo ++ } ++ }, types_js_1.InitializeResultSchema, options); ++ if (result === undefined) { ++ throw new Error(`Server sent invalid initialize result: ${result}`); ++ } ++ if (!types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) { ++ throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`); ++ } ++ this._serverCapabilities = result.capabilities; ++ this._serverVersion = result.serverInfo; ++ // HTTP transports must set the protocol version in each header after initialization. ++ if (transport.setProtocolVersion) { ++ transport.setProtocolVersion(result.protocolVersion); ++ } ++ this._instructions = result.instructions; ++ await this.notification({ ++ method: 'notifications/initialized' ++ }); ++ // Set up list changed handlers now that we know server capabilities ++ if (this._pendingListChangedConfig) { ++ this._setupListChangedHandlers(this._pendingListChangedConfig); ++ this._pendingListChangedConfig = undefined; ++ } ++ } + /** + * After initialization has completed, this will be populated with the server's reported capabilities. + */ +@@ -541,9 +547,11 @@ class Client extends protocol_js_1.Protocol { + * Called after listTools() to pre-compile validators for better performance. + */ +- cacheToolMetadata(tools) { +- this._cachedToolOutputValidators.clear(); +- this._cachedKnownTaskTools.clear(); +- this._cachedRequiredTaskTools.clear(); ++ cacheToolMetadata(tools, reset = true) { ++ if (reset) { ++ this._cachedToolOutputValidators.clear(); ++ this._cachedKnownTaskTools.clear(); ++ this._cachedRequiredTaskTools.clear(); ++ } + for (const tool of tools) { + // If the tool has an outputSchema, create and cache the validator + if (tool.outputSchema) { +@@ -569,7 +577,7 @@ class Client extends protocol_js_1.Protocol { + async listTools(params, options) { + const result = await this.request({ method: 'tools/list', params }, types_js_1.ListToolsResultSchema, options); + // Cache the tools and their output schemas for future validation +- this.cacheToolMetadata(result.tools); ++ this.cacheToolMetadata(result.tools, params?.cursor === undefined); + return result; + } + /** +diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp.js +index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eedff5443c9 100644 +--- a/dist/cjs/client/streamableHttp.js ++++ b/dist/cjs/client/streamableHttp.js +@@ -290,7 +290,38 @@ class StreamableHTTPClientTransport { + this.onclose?.(); + } + async send(message, options) { ++ return this._send(message, options, false); ++ } ++ async _recoverSession(expiredSessionId) { ++ if (this._sessionRecovery) { ++ await this._sessionRecovery; ++ return true; ++ } ++ if (this._sessionId !== expiredSessionId) ++ return true; ++ this._sessionId = undefined; ++ this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired?.()); + try { ++ await this._sessionRecovery; ++ } ++ catch (error) { ++ this._sessionId = undefined; ++ await this.close(); ++ throw error; ++ } ++ finally { ++ this._sessionRecovery = undefined; ++ } ++ return true; ++ } ++ async _send(message, options, isSessionRetry) { ++ try { ++ if (this._sessionRecovery && !(0, types_js_1.isInitializeRequest)(message) && !(0, types_js_1.isInitializedNotification)(message)) { ++ await this._sessionRecovery; ++ if (options?.isRequestActive?.() === false) { ++ throw new Error('Request is no longer active'); ++ } ++ } + const { resumptionToken, onresumptiontoken } = options || {}; + if (resumptionToken) { + // If we have at last event ID, we need to reconnect the SSE stream +@@ -298,6 +329,7 @@ class StreamableHTTPClientTransport { + return; + } + const headers = await this._commonHeaders(); ++ const requestSessionId = headers.get('mcp-session-id') ?? undefined; + headers.set('content-type', 'application/json'); + headers.set('accept', 'application/json, text/event-stream'); + const init = { +@@ -310,11 +342,20 @@ class StreamableHTTPClientTransport { + const response = await (this._fetch ?? fetch)(this._url, init); + // Handle session ID received during initialization + const sessionId = response.headers.get('mcp-session-id'); +- if (sessionId) { ++ if (sessionId && (requestSessionId === undefined || this._sessionId === requestSessionId)) { + this._sessionId = sessionId; + } + if (!response.ok) { + const text = await response.text().catch(() => null); ++ if (response.status === 404 && requestSessionId && !isSessionRetry && !(0, types_js_1.isInitializedNotification)(message)) { ++ const recovered = await this._recoverSession(requestSessionId); ++ if (options?.isRequestActive?.() === false) { ++ throw new Error('Request is no longer active'); ++ } ++ if (recovered) { ++ return this._send(message, options, true); ++ } ++ } + if (response.status === 401 && this._authProvider) { + // Prevent infinite recursion when server returns 401 after successful auth + if (this._hasCompletedAuthFlow) { +@@ -335,7 +376,7 @@ class StreamableHTTPClientTransport { + // Mark that we completed auth flow + this._hasCompletedAuthFlow = true; + // Purposely _not_ awaited, so we don't call onerror twice +- return this.send(message); ++ return this._send(message, options, isSessionRetry); + } + if (response.status === 403 && this._authProvider) { + const { resourceMetadataUrl, scope, error } = (0, auth_js_1.extractWWWAuthenticateParams)(response); +@@ -362,7 +403,7 @@ class StreamableHTTPClientTransport { + if (result !== 'AUTHORIZED') { + throw new auth_js_1.UnauthorizedError(); + } +- return this.send(message); ++ return this._send(message, options, isSessionRetry); + } + } + throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`); +diff --git a/dist/cjs/shared/protocol.js b/dist/cjs/shared/protocol.js +index 3617e787f0ba70447c99501aee7aa67584d89758..4a96d6a0328fa348b96f3869ab7e0bb77538182b 100644 +--- a/dist/cjs/shared/protocol.js ++++ b/dist/cjs/shared/protocol.js +@@ -744,7 +744,12 @@ class Protocol { + } + else { + // No related task - send through transport normally +- this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => { ++ this._transport.send(jsonrpcRequest, { ++ relatedRequestId, ++ resumptionToken, ++ onresumptiontoken, ++ isRequestActive: () => this._responseHandlers.has(messageId) ++ }).catch(error => { + this._cleanupTimeout(messageId); + reject(error); + }); +diff --git a/dist/cjs/client/auth.d.ts b/dist/cjs/client/auth.d.ts +index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644 +--- a/dist/cjs/client/auth.d.ts ++++ b/dist/cjs/client/auth.d.ts +@@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise; ++/** ++ * Selects scopes per the MCP spec and augments them for refresh token support. ++ */ ++export declare function determineScope(options: { ++ requestedScope?: string; ++ resourceMetadata?: OAuthProtectedResourceMetadata; ++ authServerMetadata?: AuthorizationServerMetadata; ++ clientMetadata: OAuthClientMetadata; ++}): string | undefined; + /** + * Orchestrates the full auth flow with a server. + * +diff --git a/dist/cjs/client/auth.js b/dist/cjs/client/auth.js +index c2e4fa91d26f5336889f6afa416147db75fc4872..178d7cfd96412d53bc14bbc13a8f76c11f727ee7 100644 +--- a/dist/cjs/client/auth.js ++++ b/dist/cjs/client/auth.js +@@ -7,6 +7,7 @@ exports.UnauthorizedError = void 0; + exports.selectClientAuthMethod = selectClientAuthMethod; + exports.parseErrorResponse = parseErrorResponse; + exports.auth = auth; ++exports.determineScope = determineScope; + exports.isHttpsUrl = isHttpsUrl; + exports.selectResourceURL = selectResourceURL; + exports.extractWWWAuthenticateParams = extractWWWAuthenticateParams; +@@ -186,6 +187,19 @@ async function auth(provider, options) { + throw error; + } + } ++/** ++ * Selects scopes per the MCP spec and augments them for refresh token support. ++ */ ++function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) { ++ let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope; ++ if (effectiveScope && ++ authServerMetadata?.scopes_supported?.includes('offline_access') && ++ !effectiveScope.split(' ').includes('offline_access') && ++ clientMetadata.grant_types?.includes('refresh_token')) { ++ effectiveScope = `${effectiveScope} offline_access`; ++ } ++ return effectiveScope; ++} + async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) { + // Check if the provider has cached discovery state to skip discovery + const cachedState = await provider.discoveryState?.(); +@@ -241,12 +255,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res + }); + } + const resource = await selectResourceURL(serverUrl, provider, resourceMetadata); +- // Apply scope selection strategy (SEP-835): +- // 1. WWW-Authenticate scope (passed via `scope` param) +- // 2. PRM scopes_supported +- // 3. Client metadata scope (user-configured fallback) +- // The resolved scope is used consistently for both DCR and the authorization request. +- const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope; ++ const resolvedScope = determineScope({ ++ requestedScope: scope, ++ resourceMetadata, ++ authServerMetadata: metadata, ++ clientMetadata: provider.clientMetadata ++ }); + // Handle client registration if needed + let clientInformation = await Promise.resolve(provider.clientInformation()); + if (!clientInformation) { +@@ -741,7 +755,7 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo + if (scope) { + authorizationUrl.searchParams.set('scope', scope); + } +- if (scope?.includes('offline_access')) { ++ if (scope?.split(' ').includes('offline_access')) { + // if the request includes the OIDC-only "offline_access" scope, + // we need to set the prompt to "consent" to ensure the user is prompted to grant offline access + // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess +diff --git a/dist/esm/client/auth.d.ts b/dist/esm/client/auth.d.ts +index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644 +--- a/dist/esm/client/auth.d.ts ++++ b/dist/esm/client/auth.d.ts +@@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise; ++/** ++ * Selects scopes per the MCP spec and augments them for refresh token support. ++ */ ++export declare function determineScope(options: { ++ requestedScope?: string; ++ resourceMetadata?: OAuthProtectedResourceMetadata; ++ authServerMetadata?: AuthorizationServerMetadata; ++ clientMetadata: OAuthClientMetadata; ++}): string | undefined; + /** + * Orchestrates the full auth flow with a server. + * +diff --git a/dist/esm/client/auth.js b/dist/esm/client/auth.js +index e183040fc2bba22ca1ccc784984f3310854403b7..d367661e580ee61a96654f7af78b2af61dcad98b 100644 +--- a/dist/esm/client/auth.js ++++ b/dist/esm/client/auth.js +@@ -161,6 +161,19 @@ export async function auth(provider, options) { + throw error; + } + } ++/** ++ * Selects scopes per the MCP spec and augments them for refresh token support. ++ */ ++export function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) { ++ let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope; ++ if (effectiveScope && ++ authServerMetadata?.scopes_supported?.includes('offline_access') && ++ !effectiveScope.split(' ').includes('offline_access') && ++ clientMetadata.grant_types?.includes('refresh_token')) { ++ effectiveScope = `${effectiveScope} offline_access`; ++ } ++ return effectiveScope; ++} + async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) { + // Check if the provider has cached discovery state to skip discovery + const cachedState = await provider.discoveryState?.(); +@@ -216,12 +229,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res + }); + } + const resource = await selectResourceURL(serverUrl, provider, resourceMetadata); +- // Apply scope selection strategy (SEP-835): +- // 1. WWW-Authenticate scope (passed via `scope` param) +- // 2. PRM scopes_supported +- // 3. Client metadata scope (user-configured fallback) +- // The resolved scope is used consistently for both DCR and the authorization request. +- const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope; ++ const resolvedScope = determineScope({ ++ requestedScope: scope, ++ resourceMetadata, ++ authServerMetadata: metadata, ++ clientMetadata: provider.clientMetadata ++ }); + // Handle client registration if needed + let clientInformation = await Promise.resolve(provider.clientInformation()); + if (!clientInformation) { +@@ -716,7 +729,7 @@ export async function startAuthorization(authorizationServerUrl, { metadata, cli + if (scope) { + authorizationUrl.searchParams.set('scope', scope); + } +- if (scope?.includes('offline_access')) { ++ if (scope?.split(' ').includes('offline_access')) { + // if the request includes the OIDC-only "offline_access" scope, + // we need to set the prompt to "consent" to ensure the user is prompted to grant offline access + // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess +diff --git a/dist/esm/client/index.js b/dist/esm/client/index.js +index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8eb9e4caff 100644 +--- a/dist/esm/client/index.js ++++ b/dist/esm/client/index.js +@@ -284,41 +284,16 @@ export class Client extends Protocol { + } + async connect(transport, options) { + await super.connect(transport); ++ transport.onsessionexpired = async () => { ++ await this._initialize(transport); ++ }; + // When transport sessionId is already set this means we are trying to reconnect. + // In this case we don't need to initialize again. + if (transport.sessionId !== undefined) { + return; + } + try { +- const result = await this.request({ +- method: 'initialize', +- params: { +- protocolVersion: LATEST_PROTOCOL_VERSION, +- capabilities: this._capabilities, +- clientInfo: this._clientInfo +- } +- }, InitializeResultSchema, options); +- if (result === undefined) { +- throw new Error(`Server sent invalid initialize result: ${result}`); +- } +- if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) { +- throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`); +- } +- this._serverCapabilities = result.capabilities; +- this._serverVersion = result.serverInfo; +- // HTTP transports must set the protocol version in each header after initialization. +- if (transport.setProtocolVersion) { +- transport.setProtocolVersion(result.protocolVersion); +- } +- this._instructions = result.instructions; +- await this.notification({ +- method: 'notifications/initialized' +- }); +- // Set up list changed handlers now that we know server capabilities +- if (this._pendingListChangedConfig) { +- this._setupListChangedHandlers(this._pendingListChangedConfig); +- this._pendingListChangedConfig = undefined; +- } ++ await this._initialize(transport, options); + } + catch (error) { + // Disconnect if initialization fails. +@@ -326,6 +301,37 @@ export class Client extends Protocol { + throw error; + } + } ++ async _initialize(transport, options) { ++ const result = await this.request({ ++ method: 'initialize', ++ params: { ++ protocolVersion: LATEST_PROTOCOL_VERSION, ++ capabilities: this._capabilities, ++ clientInfo: this._clientInfo ++ } ++ }, InitializeResultSchema, options); ++ if (result === undefined) { ++ throw new Error(`Server sent invalid initialize result: ${result}`); ++ } ++ if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) { ++ throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`); ++ } ++ this._serverCapabilities = result.capabilities; ++ this._serverVersion = result.serverInfo; ++ // HTTP transports must set the protocol version in each header after initialization. ++ if (transport.setProtocolVersion) { ++ transport.setProtocolVersion(result.protocolVersion); ++ } ++ this._instructions = result.instructions; ++ await this.notification({ ++ method: 'notifications/initialized' ++ }); ++ // Set up list changed handlers now that we know server capabilities ++ if (this._pendingListChangedConfig) { ++ this._setupListChangedHandlers(this._pendingListChangedConfig); ++ this._pendingListChangedConfig = undefined; ++ } ++ } + /** + * After initialization has completed, this will be populated with the server's reported capabilities. + */ +@@ -537,9 +543,11 @@ export class Client extends Protocol { + * Called after listTools() to pre-compile validators for better performance. + */ +- cacheToolMetadata(tools) { +- this._cachedToolOutputValidators.clear(); +- this._cachedKnownTaskTools.clear(); +- this._cachedRequiredTaskTools.clear(); ++ cacheToolMetadata(tools, reset = true) { ++ if (reset) { ++ this._cachedToolOutputValidators.clear(); ++ this._cachedKnownTaskTools.clear(); ++ this._cachedRequiredTaskTools.clear(); ++ } + for (const tool of tools) { + // If the tool has an outputSchema, create and cache the validator + if (tool.outputSchema) { +@@ -565,7 +573,7 @@ export class Client extends Protocol { + async listTools(params, options) { + const result = await this.request({ method: 'tools/list', params }, ListToolsResultSchema, options); + // Cache the tools and their output schemas for future validation +- this.cacheToolMetadata(result.tools); ++ this.cacheToolMetadata(result.tools, params?.cursor === undefined); + return result; + } + /** +diff --git a/dist/esm/client/streamableHttp.js b/dist/esm/client/streamableHttp.js +index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da884fcc627a 100644 +--- a/dist/esm/client/streamableHttp.js ++++ b/dist/esm/client/streamableHttp.js +@@ -1,5 +1,5 @@ + import { createFetchWithInit, normalizeHeaders } from '../shared/transport.js'; +-import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js'; ++import { isInitializedNotification, isInitializeRequest, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js'; + import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js'; + import { EventSourceParserStream } from 'eventsource-parser/stream'; + // Default reconnection options for StreamableHTTP connections +@@ -286,7 +286,38 @@ export class StreamableHTTPClientTransport { + this.onclose?.(); + } + async send(message, options) { ++ return this._send(message, options, false); ++ } ++ async _recoverSession(expiredSessionId) { ++ if (this._sessionRecovery) { ++ await this._sessionRecovery; ++ return true; ++ } ++ if (this._sessionId !== expiredSessionId) ++ return true; ++ this._sessionId = undefined; ++ this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired?.()); + try { ++ await this._sessionRecovery; ++ } ++ catch (error) { ++ this._sessionId = undefined; ++ await this.close(); ++ throw error; ++ } ++ finally { ++ this._sessionRecovery = undefined; ++ } ++ return true; ++ } ++ async _send(message, options, isSessionRetry) { ++ try { ++ if (this._sessionRecovery && !isInitializeRequest(message) && !isInitializedNotification(message)) { ++ await this._sessionRecovery; ++ if (options?.isRequestActive?.() === false) { ++ throw new Error('Request is no longer active'); ++ } ++ } + const { resumptionToken, onresumptiontoken } = options || {}; + if (resumptionToken) { + // If we have at last event ID, we need to reconnect the SSE stream +@@ -294,6 +325,7 @@ export class StreamableHTTPClientTransport { + return; + } + const headers = await this._commonHeaders(); ++ const requestSessionId = headers.get('mcp-session-id') ?? undefined; + headers.set('content-type', 'application/json'); + headers.set('accept', 'application/json, text/event-stream'); + const init = { +@@ -306,11 +338,20 @@ export class StreamableHTTPClientTransport { + const response = await (this._fetch ?? fetch)(this._url, init); + // Handle session ID received during initialization + const sessionId = response.headers.get('mcp-session-id'); +- if (sessionId) { ++ if (sessionId && (requestSessionId === undefined || this._sessionId === requestSessionId)) { + this._sessionId = sessionId; + } + if (!response.ok) { + const text = await response.text().catch(() => null); ++ if (response.status === 404 && requestSessionId && !isSessionRetry && !isInitializedNotification(message)) { ++ const recovered = await this._recoverSession(requestSessionId); ++ if (options?.isRequestActive?.() === false) { ++ throw new Error('Request is no longer active'); ++ } ++ if (recovered) { ++ return this._send(message, options, true); ++ } ++ } + if (response.status === 401 && this._authProvider) { + // Prevent infinite recursion when server returns 401 after successful auth + if (this._hasCompletedAuthFlow) { +@@ -331,7 +372,7 @@ export class StreamableHTTPClientTransport { + // Mark that we completed auth flow + this._hasCompletedAuthFlow = true; + // Purposely _not_ awaited, so we don't call onerror twice +- return this.send(message); ++ return this._send(message, options, isSessionRetry); + } + if (response.status === 403 && this._authProvider) { + const { resourceMetadataUrl, scope, error } = extractWWWAuthenticateParams(response); +@@ -358,7 +399,7 @@ export class StreamableHTTPClientTransport { + if (result !== 'AUTHORIZED') { + throw new UnauthorizedError(); + } +- return this.send(message); ++ return this._send(message, options, isSessionRetry); + } + } + throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`); +diff --git a/dist/esm/shared/protocol.js b/dist/esm/shared/protocol.js +index bfa2b7120a0f50c569364ea5264e6f811076f44f..abd8dfd707c155f71dae7aeeeeaf7547368ac749 100644 +--- a/dist/esm/shared/protocol.js ++++ b/dist/esm/shared/protocol.js +@@ -740,7 +740,12 @@ export class Protocol { + } + else { + // No related task - send through transport normally +- this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => { ++ this._transport.send(jsonrpcRequest, { ++ relatedRequestId, ++ resumptionToken, ++ onresumptiontoken, ++ isRequestActive: () => this._responseHandlers.has(messageId) ++ }).catch(error => { + this._cleanupTimeout(messageId); + reject(error); + }); From d8a9d8a76d621101376cc9334ae298f0a12c0771 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 28 Jul 2026 16:55:30 +0000 Subject: [PATCH 067/129] chore: generate --- packages/sdk/openapi.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 6150b75e64..b300754bc8 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -3360,9 +3360,6 @@ "properties": { "code": { "type": "string" - }, - "iss": { - "type": "string" } }, "required": ["code"], From f28d72d15e00f51f7e30f9cbf08b810f292bb3ee Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 28 Jul 2026 17:12:42 +0000 Subject: [PATCH 068/129] 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 d1bd9c5131..62788beafd 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-wLjPbbeOF9OUNNGceYtEJNiBtF6TPjX/lnyMkdW170c=", - "aarch64-linux": "sha256-P7hrfVp3pILY/uAOdJweGg6IcXBLuU69yqko1qd1caw=", - "aarch64-darwin": "sha256-MJzCshCjkbypnW3yhmR8/zCadIopcHUfVT293N0FMY8=", - "x86_64-darwin": "sha256-6fR137gR66eI+VyvDkR2U51yoJ9BuvaK4YoMw+3E2A0=" + "x86_64-linux": "sha256-/ZtluLolVq1ZFAYgbDScze9GZR0NF9baRQXvfEoOnkI=", + "aarch64-linux": "sha256-kdh7Dpira7cx9lvtozFsjLrDqMBrUMcgxkhfsqrutws=", + "aarch64-darwin": "sha256-chPeMCX+8hvOQ32hTANIn7FmONsSv9t3dKTWTaHerjs=", + "x86_64-darwin": "sha256-rMc0dDZB+YbXrjYwXTIt7QvHUxSp//wDJQ5hUdR+LYM=" } } From a45c2b917e657e50881117e8c3f85f4bff06e47d Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 28 Jul 2026 18:14:59 +0000 Subject: [PATCH 069/129] sync release versions for v1.18.9 --- bun.lock | 56 ++++++++++----------- packages/app/package.json | 2 +- packages/cli/package.json | 2 +- packages/codemode/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/console/support/package.json | 2 +- packages/core/package.json | 2 +- packages/desktop/package.json | 2 +- packages/effect-drizzle-sqlite/package.json | 2 +- packages/effect-sqlite-node/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/function/package.json | 2 +- packages/http-recorder/package.json | 2 +- packages/llm/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/server/package.json | 2 +- packages/session-ui/package.json | 2 +- packages/slack/package.json | 2 +- packages/stats/app/package.json | 2 +- packages/stats/core/package.json | 2 +- packages/stats/server/package.json | 2 +- packages/tui/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 30 files changed, 57 insertions(+), 57 deletions(-) diff --git a/bun.lock b/bun.lock index 96458854bc..19f5a97cb2 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", @@ -96,7 +96,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.18.8", + "version": "1.18.9", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -144,7 +144,7 @@ }, "packages/codemode": { "name": "@opencode-ai/codemode", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "acorn": "8.15.0", "effect": "catalog:", @@ -158,7 +158,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -194,7 +194,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -221,7 +221,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -243,7 +243,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -267,7 +267,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -287,7 +287,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.18.8", + "version": "1.18.9", "bin": { "opencode": "./bin/opencode", }, @@ -381,7 +381,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@zip.js/zip.js": "2.7.62", "effect": "catalog:", @@ -435,7 +435,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -449,7 +449,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "effect": "catalog:", }, @@ -461,7 +461,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -493,7 +493,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -509,7 +509,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83", @@ -540,7 +540,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", @@ -559,7 +559,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.18.8", + "version": "1.18.9", "bin": { "opencode": "./bin/opencode", }, @@ -690,7 +690,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "workspace:*", @@ -766,7 +766,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "cross-spawn": "catalog:", }, @@ -781,7 +781,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", @@ -796,7 +796,7 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", @@ -841,7 +841,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -854,7 +854,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@ibm/plex": "6.4.1", "@kobalte/core": "catalog:", @@ -888,7 +888,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -907,7 +907,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -949,7 +949,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -976,7 +976,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1027,7 +1027,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.18.8", + "version": "1.18.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 73840b2dbf..7199ad9425 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.18.8", + "version": "1.18.9", "description": "", "type": "module", "exports": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 45271e869c..d64acf4840 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/cli", - "version": "1.18.8", + "version": "1.18.9", "type": "module", "license": "MIT", "bin": { diff --git a/packages/codemode/package.json b/packages/codemode/package.json index 51df6e53a5..83cfeaac0c 100644 --- a/packages/codemode/package.json +++ b/packages/codemode/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/codemode", - "version": "1.18.8", + "version": "1.18.9", "description": "Effect-native confined code execution over schema-described tools", "private": true, "type": "module", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 446c3d0854..b8fd9a04c6 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.18.8", + "version": "1.18.9", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index f5a1bbb72a..f922ab9ef2 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.18.8", + "version": "1.18.9", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 937c0cf538..aadfa42be1 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.18.8", + "version": "1.18.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 531b0ec651..1a551ed0a5 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.18.8", + "version": "1.18.9", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/console/support/package.json b/packages/console/support/package.json index a556e236bb..6d90a394cb 100644 --- a/packages/console/support/package.json +++ b/packages/console/support/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-support", - "version": "1.18.8", + "version": "1.18.9", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index c1635e6323..2325bcc01e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.8", + "version": "1.18.9", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 2f3b90ae9b..1a5baf91b0 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.18.8", + "version": "1.18.9", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index a3e9ea8814..6ae8d912a1 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.8", + "version": "1.18.9", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/effect-sqlite-node/package.json b/packages/effect-sqlite-node/package.json index 507ad6769e..d26bea7f85 100644 --- a/packages/effect-sqlite-node/package.json +++ b/packages/effect-sqlite-node/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.8", + "version": "1.18.9", "name": "@opencode-ai/effect-sqlite-node", "type": "module", "license": "MIT", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 7a4c65783f..d298dd1dee 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.18.8", + "version": "1.18.9", "private": true, "type": "module", "license": "MIT", diff --git a/packages/function/package.json b/packages/function/package.json index e7f8516161..6acd31710e 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.18.8", + "version": "1.18.9", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index 1eaf4333fc..cca67e0c30 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.8", + "version": "1.18.9", "name": "@opencode-ai/http-recorder", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "type": "module", diff --git a/packages/llm/package.json b/packages/llm/package.json index 6d3a826e59..69e1ea92eb 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.8", + "version": "1.18.9", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 8f7af65a8f..b7ee6973d8 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.8", + "version": "1.18.9", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index fefab272f5..2607c317d6 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.18.8", + "version": "1.18.9", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 8e8c19d73b..f92dca0c4d 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.18.8", + "version": "1.18.9", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 4532b2e37b..89d7360a79 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/server", - "version": "1.18.8", + "version": "1.18.9", "private": true, "type": "module", "license": "MIT", diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index 879f254df1..eb67c17d19 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/session-ui", - "version": "1.18.8", + "version": "1.18.9", "private": true, "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index 5636890bb2..7ec9f938b7 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.18.8", + "version": "1.18.9", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json index aceecc49d5..8e69fd7193 100644 --- a/packages/stats/app/package.json +++ b/packages/stats/app/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-app", - "version": "1.18.8", + "version": "1.18.9", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/core/package.json b/packages/stats/core/package.json index 73de8bbced..45e6acf976 100644 --- a/packages/stats/core/package.json +++ b/packages/stats/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-core", - "version": "1.18.8", + "version": "1.18.9", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/server/package.json b/packages/stats/server/package.json index af0f57d187..5d34525aa1 100644 --- a/packages/stats/server/package.json +++ b/packages/stats/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-server", - "version": "1.18.8", + "version": "1.18.9", "private": true, "type": "module", "license": "MIT", diff --git a/packages/tui/package.json b/packages/tui/package.json index 8207147917..9900a37849 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/tui", - "version": "1.18.8", + "version": "1.18.9", "private": true, "type": "module", "license": "MIT", diff --git a/packages/ui/package.json b/packages/ui/package.json index e5e7a15a1c..05c4ffbf63 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.18.8", + "version": "1.18.9", "type": "module", "license": "MIT", "repository": { diff --git a/packages/web/package.json b/packages/web/package.json index 963e9dfc5f..bdc41cdd67 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.18.8", + "version": "1.18.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 a5948bb436..33c79c001d 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.18.8", + "version": "1.18.9", "publisher": "sst-dev", "repository": { "type": "git", From f256a4c538a2b13f9800fa9a846cc03ffad2b0ca Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:24:31 +0800 Subject: [PATCH 070/129] fix(app): preserve agent picker for existing users (#39300) --- packages/app/src/context/settings.test.ts | 17 +++++++++++ packages/app/src/context/settings.tsx | 30 ++++++++++++++++++-- packages/desktop/src/renderer/onboarding.tsx | 1 + 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/app/src/context/settings.test.ts b/packages/app/src/context/settings.test.ts index 3f94f22ec3..51b35eacbb 100644 --- a/packages/app/src/context/settings.test.ts +++ b/packages/app/src/context/settings.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test" import { hasExistingWebState, + initialAgentVisibility, isAppUpgrade, layoutTransitionState, maximumSunsetTimeout, @@ -11,6 +12,22 @@ import { shouldEnableNewLayout, } from "./settings" +describe("agent visibility", () => { + test("shows the picker for existing profiles and hides it for first-time installs", () => { + expect(initialAgentVisibility(undefined, true)).toBe(true) + expect(initialAgentVisibility(undefined, false)).toBe(false) + }) + + test("shows the picker when updating from a recent release", () => { + expect(initialAgentVisibility(undefined, false, "1.18.8")).toBe(true) + }) + + test("preserves the preference after initialization", () => { + expect(initialAgentVisibility(true, true, "1.18.8")).toBeUndefined() + expect(initialAgentVisibility(true, false)).toBeUndefined() + }) +}) + describe("layout transition", () => { test("blank profiles default to the new layout", () => { expect(newLayoutDesignsDefault).toBe(true) diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index fe8b4e3c03..23dbb53174 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -1,5 +1,5 @@ import { createStore, reconcile } from "solid-js/store" -import { createEffect, createMemo, createSignal, onCleanup } from "solid-js" +import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" import { persisted } from "@/utils/persist" import { usePlatform } from "@/context/platform" @@ -36,6 +36,7 @@ export interface Settings { mobileTitlebarPosition: "top" | "bottom" newLayoutDesigns?: boolean layoutTransitionEligible?: boolean + agentVisibilityInitialized?: boolean newInterfaceNoticeDismissed?: boolean shouldDisplayTabsToast?: boolean } @@ -93,6 +94,15 @@ export function hasExistingWebState(settings: Promise | string | null, p return settings !== null || previousVersion !== undefined } +export function initialAgentVisibility( + initialized: boolean | undefined, + existing: boolean, + previousVersion?: string, +) { + if (initialized === true) return + return existing || previousVersion !== undefined +} + export function shouldEnableNewLayout(previous: string | undefined, current: string | undefined) { if (!current) return false const currentComparison = compareVersions(current, newLayoutDesignsUpgradeCutoff) @@ -271,6 +281,18 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont ) }) const visible = (preference: () => boolean) => createMemo(() => !newLayoutDesigns() || preference()) + const initializeAgentVisibility = (existing: boolean) => { + const initial = initialAgentVisibility( + store.general?.agentVisibilityInitialized, + existing, + launchState.previous, + ) + if (initial === undefined) return + batch(() => { + setStore("general", "showCustomAgents", initial) + setStore("general", "agentVisibilityInitialized", true) + }) + } if (sunset && !oldInterfaceRetired()) { const timeout = { current: undefined as ReturnType | undefined } @@ -299,8 +321,9 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont createEffect(() => { if (!ready() || !launchState.classified || platform.platform !== "web") return - if (layoutTransitionClassified()) return - setStore("general", "layoutTransitionEligible", hasExistingWebState(settingsInit, launchState.previous)) + const existing = hasExistingWebState(settingsInit, launchState.previous) + if (!layoutTransitionClassified()) setStore("general", "layoutTransitionEligible", existing) + initializeAgentVisibility(existing) }) createEffect(() => { @@ -426,6 +449,7 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont if (typeof current === "boolean") return setStore("general", "layoutTransitionEligible", eligible) }, + initializeAgentVisibility, layoutTransitionAvailable: createMemo(() => ready() && layoutTransition().available), newInterfaceNoticeVisible: createMemo(() => ready() && layoutTransition().notice), dismissNewInterfaceNotice() { diff --git a/packages/desktop/src/renderer/onboarding.tsx b/packages/desktop/src/renderer/onboarding.tsx index 76c7ff683a..c5d7ff5389 100644 --- a/packages/desktop/src/renderer/onboarding.tsx +++ b/packages/desktop/src/renderer/onboarding.tsx @@ -17,6 +17,7 @@ export function DesktopFirstLaunchOnboarding(props: { initialUrl: string; onLoad ) const existingInstall = await window.api.isOldLayoutEligible() settings.general.setOldLayoutEligible(existingInstall) + settings.general.initializeAgentVisibility(existingInstall) if (!server.isLocal()) return const pending = await window.api.isFirstLaunchOnboardingPending() From 8cbea4fbb7f2a8ccf59f44922ef7c1ff5f22e377 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 29 Jul 2026 01:26:18 +0000 Subject: [PATCH 071/129] chore: generate --- packages/app/src/context/settings.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index 23dbb53174..1a118b6544 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -94,11 +94,7 @@ export function hasExistingWebState(settings: Promise | string | null, p return settings !== null || previousVersion !== undefined } -export function initialAgentVisibility( - initialized: boolean | undefined, - existing: boolean, - previousVersion?: string, -) { +export function initialAgentVisibility(initialized: boolean | undefined, existing: boolean, previousVersion?: string) { if (initialized === true) return return existing || previousVersion !== undefined } @@ -282,11 +278,7 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont }) const visible = (preference: () => boolean) => createMemo(() => !newLayoutDesigns() || preference()) const initializeAgentVisibility = (existing: boolean) => { - const initial = initialAgentVisibility( - store.general?.agentVisibilityInitialized, - existing, - launchState.previous, - ) + const initial = initialAgentVisibility(store.general?.agentVisibilityInitialized, existing, launchState.previous) if (initial === undefined) return batch(() => { setStore("general", "showCustomAgents", initial) From e8b09927889ba4b5b7fc74bbab5b864d205406ca Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:16:30 +0800 Subject: [PATCH 072/129] fix(app): defer model variant selector (#39445) --- packages/app/src/components/prompt-input-v2.tsx | 1 + packages/session-ui/src/v2/components/prompt-input/index.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/prompt-input-v2.tsx b/packages/app/src/components/prompt-input-v2.tsx index 44d9d48d38..634e90f78f 100644 --- a/packages/app/src/components/prompt-input-v2.tsx +++ b/packages/app/src/components/prompt-input-v2.tsx @@ -55,6 +55,7 @@ export function PromptInputV2Composer(props: PromptInputV2ComposerProps) { controller={props.controller} borderUnderlay={props.borderUnderlay} class={props.class} + variantControlVisible={!props.controller.model.loading} attachKeybind={command.keybindParts("file.attach")} attachShortcut={command.keybind("file.attach")} modelControl={ diff --git a/packages/session-ui/src/v2/components/prompt-input/index.tsx b/packages/session-ui/src/v2/components/prompt-input/index.tsx index 9b011f03e9..c09b5a1b5f 100644 --- a/packages/session-ui/src/v2/components/prompt-input/index.tsx +++ b/packages/session-ui/src/v2/components/prompt-input/index.tsx @@ -39,6 +39,7 @@ export type PromptInputV2Props = { borderUnderlay?: boolean class?: string modelControl?: JSX.Element + variantControlVisible?: boolean attachKeybind?: string[] attachShortcut?: string } @@ -232,7 +233,7 @@ export function PromptInputV2(props: PromptInputV2Props) { > {props.modelControl}
- + {(control) => ( 1}> Date: Wed, 29 Jul 2026 10:34:37 +0200 Subject: [PATCH 073/129] feat(desktop): refine tab states (#39472) --- .../app/src/components/titlebar-tab-nav.css | 74 ++++++++++++++----- .../app/src/components/titlebar-tab-nav.tsx | 12 +-- 2 files changed, 62 insertions(+), 24 deletions(-) diff --git a/packages/app/src/components/titlebar-tab-nav.css b/packages/app/src/components/titlebar-tab-nav.css index 08bed7cf26..95fe212a5d 100644 --- a/packages/app/src/components/titlebar-tab-nav.css +++ b/packages/app/src/components/titlebar-tab-nav.css @@ -9,53 +9,91 @@ justify-content: center; } +[data-titlebar-tab] { + --tab-base: var(--v2-background-bg-deep); + --tab-overlay: transparent; + background: + linear-gradient(var(--tab-overlay), var(--tab-overlay)), + var(--tab-base); +} + +[data-titlebar-tab]:is(:hover, :has(> [data-slot="tab-link"]:focus-visible)):not([data-state="pressed"]):not( + [data-dragging="true"] + ):not([data-editing="true"]) { + --tab-base: var(--v2-background-bg-layer-02); + --tab-overlay: var(--v2-overlay-simple-overlay-hover); +} + +[data-titlebar-tab]:is([data-state="pressed"], [data-dragging="true"], [data-editing="true"]) { + --tab-base: var(--v2-background-bg-layer-02); + --tab-overlay: var(--v2-overlay-simple-overlay-pressed); +} + +[data-titlebar-tab]:is(:hover, [data-state="pressed"]) [data-slot="tab-close"] { + background: + linear-gradient(var(--tab-overlay), var(--tab-overlay)), + var(--tab-base); +} + [data-titlebar-tab][data-editing="true"] [data-slot="tab-close"] { display: none; } [data-titlebar-tab-list] { - gap: 13.5px; + gap: 6px; } [data-titlebar-tab-slot] { + --tab-separator: var(--v2-background-bg-layer-03); position: relative; } +[data-color-scheme="dark"] [data-titlebar-tab-slot] { + --tab-separator: var(--v2-background-bg-layer-02); +} + [data-titlebar-tab-slot]:not(:first-child):not([data-active="true"])::before { content: ""; position: absolute; top: 8px; - left: -6.75px; + left: -3.75px; width: 1.5px; height: 12px; border-radius: 9999px; - background: var(--v2-background-bg-layer-02); + background: var(--tab-separator); } [data-titlebar-tab-slot][data-active="true"] + [data-titlebar-tab-slot]::before { display: none; } -[data-titlebar-tab] [data-slot="tab-close"]::before { - content: ""; - position: absolute; - top: 0; - height: 28px; - width: 16px; - pointer-events: none; - background: linear-gradient(to right, transparent, var(--tab-bg)); +[data-titlebar-tab-slot]:not([data-active="true"]):has([data-titlebar-tab]:hover)::before, +[data-titlebar-tab-slot]:not([data-active="true"]):has([data-titlebar-tab]:hover) + + [data-titlebar-tab-slot]::before { display: none; } -[data-titlebar-tab][data-title-overflow="true"]:not([data-editing="true"]) [data-slot="tab-close"]::before { - display: block; - right: 0; +[data-titlebar-tab][data-title-overflow="true"]:not([data-editing="true"]) [data-slot="tab-link"] { + --tab-title-fade-offset: 4px; + -webkit-mask-image: linear-gradient( + to right, + black 0, + black calc(100% - var(--tab-title-fade-offset) - 16px), + transparent calc(100% - var(--tab-title-fade-offset)), + transparent 100% + ); + mask-image: linear-gradient( + to right, + black 0, + black calc(100% - var(--tab-title-fade-offset) - 16px), + transparent calc(100% - var(--tab-title-fade-offset)), + transparent 100% + ); } -[data-titlebar-tab][data-title-overflow="true"]:hover:not([data-editing="true"]) [data-slot="tab-close"]::before, -[data-titlebar-tab][data-title-overflow="true"][data-active="true"]:not([data-editing="true"]) - [data-slot="tab-close"]::before { - right: 100%; +[data-titlebar-tab][data-title-overflow="true"]:is(:hover, [data-active="true"]):not([data-editing="true"]) + [data-slot="tab-link"] { + --tab-title-fade-offset: 24px; } [data-titlebar-tab][data-title-overflow="true"]:not(:hover):not([data-active="true"]):not([data-editing="true"]) diff --git a/packages/app/src/components/titlebar-tab-nav.tsx b/packages/app/src/components/titlebar-tab-nav.tsx index 35f600c9f6..3823cf10b8 100644 --- a/packages/app/src/components/titlebar-tab-nav.tsx +++ b/packages/app/src/components/titlebar-tab-nav.tsx @@ -180,11 +180,11 @@ export function TabNavItem(props: { data-slot="titlebar-tab-item" data-title-overflow={titleOverflowing()} data-editing={editing()} - class="group relative flex h-7 w-full min-w-0 select-none flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [container-type:inline-size] [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[editing='true']:[--tab-bg:var(--v2-background-bg-layer-02)]" + class="group relative flex h-7 w-full min-w-0 select-none flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] px-1.5 [container-type:inline-size]" classList={{ invisible: props.hidden }} data-active={props.active} data-dragging={props.dragging} - data-pressed={props.pressed} + data-state={props.active || props.pressed ? "pressed" : undefined} onMouseDown={(event) => { if (event.button !== MIDDLE_MOUSE_BUTTON) return event.preventDefault() @@ -276,7 +276,7 @@ export function TabNavItem(props: { /> -
+
{ if (event.button !== MIDDLE_MOUSE_BUTTON) return @@ -381,7 +381,7 @@ export function DraftTabItem(props: { {props.title} -
+
Date: Wed, 29 Jul 2026 08:36:03 +0000 Subject: [PATCH 074/129] chore: generate --- packages/app/src/components/titlebar-tab-nav.css | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/app/src/components/titlebar-tab-nav.css b/packages/app/src/components/titlebar-tab-nav.css index 95fe212a5d..13dfbe04fc 100644 --- a/packages/app/src/components/titlebar-tab-nav.css +++ b/packages/app/src/components/titlebar-tab-nav.css @@ -12,9 +12,7 @@ [data-titlebar-tab] { --tab-base: var(--v2-background-bg-deep); --tab-overlay: transparent; - background: - linear-gradient(var(--tab-overlay), var(--tab-overlay)), - var(--tab-base); + background: linear-gradient(var(--tab-overlay), var(--tab-overlay)), var(--tab-base); } [data-titlebar-tab]:is(:hover, :has(> [data-slot="tab-link"]:focus-visible)):not([data-state="pressed"]):not( @@ -30,9 +28,7 @@ } [data-titlebar-tab]:is(:hover, [data-state="pressed"]) [data-slot="tab-close"] { - background: - linear-gradient(var(--tab-overlay), var(--tab-overlay)), - var(--tab-base); + background: linear-gradient(var(--tab-overlay), var(--tab-overlay)), var(--tab-base); } [data-titlebar-tab][data-editing="true"] [data-slot="tab-close"] { @@ -68,8 +64,7 @@ } [data-titlebar-tab-slot]:not([data-active="true"]):has([data-titlebar-tab]:hover)::before, -[data-titlebar-tab-slot]:not([data-active="true"]):has([data-titlebar-tab]:hover) - + [data-titlebar-tab-slot]::before { +[data-titlebar-tab-slot]:not([data-active="true"]):has([data-titlebar-tab]:hover) + [data-titlebar-tab-slot]::before { display: none; } From 02f50a6e803eb039af28f12efa45d0212efa2e38 Mon Sep 17 00:00:00 2001 From: Vladimir Glafirov Date: Wed, 29 Jul 2026 17:45:40 +0000 Subject: [PATCH 075/129] chore: bump gitlab-ai-provider to 6.12.1 (#39490) --- bun.lock | 6 +++--- packages/core/package.json | 2 +- packages/opencode/package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bun.lock b/bun.lock index 19f5a97cb2..aaf43212b7 100644 --- a/bun.lock +++ b/bun.lock @@ -339,7 +339,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.12.0", + "gitlab-ai-provider": "6.12.1", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -632,7 +632,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.12.0", + "gitlab-ai-provider": "6.12.1", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -3855,7 +3855,7 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "gitlab-ai-provider": ["gitlab-ai-provider@6.12.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-uGan7MQaNfmV5SH9wiMJkZ7D5QxM4KVPRcjIHtPCM5icZjgXl5mrHMjr3/e7TAHa9OjEJKT0a2FxzPgwRtN3PQ=="], + "gitlab-ai-provider": ["gitlab-ai-provider@6.12.1", "", { "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-Qn5iHqvjG8yktI5MWaUgdRR94l7O4WtYW0CAbhsCh1Tj0Fei/DeprOYPVyf4Nht1Ix6U2PXSYM32QOHI6Z2TDw=="], "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/core/package.json b/packages/core/package.json index 2325bcc01e..cc1f5d6c61 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -108,7 +108,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.12.0", + "gitlab-ai-provider": "6.12.1", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index b7ee6973d8..105f307319 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -120,7 +120,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.12.0", + "gitlab-ai-provider": "6.12.1", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", From a6f7fe739c691e8b086c50390cf3205f0b5d431e Mon Sep 17 00:00:00 2001 From: Lakshya Agarwal Date: Wed, 29 Jul 2026 13:52:12 -0400 Subject: [PATCH 076/129] docs(ecosystem): add opencode-tavily plugin (#38709) --- packages/web/src/content/docs/ecosystem.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web/src/content/docs/ecosystem.mdx b/packages/web/src/content/docs/ecosystem.mdx index 2fc3b65486..ce4f3100af 100644 --- a/packages/web/src/content/docs/ecosystem.mdx +++ b/packages/web/src/content/docs/ecosystem.mdx @@ -54,6 +54,7 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw | [opencode-firecrawl](https://github.com/firecrawl/opencode-firecrawl) | Web scraping, crawling, and search via the Firecrawl CLI | | [opencode-jfrog-plugin](https://github.com/jfrog/opencode-jfrog-plugin) | JFrog Plugin for seamless integration of Opencode users to JFrog platform | | [opencode-goal-plugin](https://github.com/willytop8/OpenCode-goal-plugin) | Session-scoped `/goal` workflow that keeps objectives in context and auto-continues until complete | +| [opencode-tavily](https://github.com/tavily-ai/opencode-tavily) | Web search, extraction, crawling, and deep research via the Tavily CLI | --- From 1e17856ba4b5b052650c8115060852f3f023844e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 29 Jul 2026 18:06:26 +0000 Subject: [PATCH 077/129] 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 62788beafd..59ba15e5b2 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-/ZtluLolVq1ZFAYgbDScze9GZR0NF9baRQXvfEoOnkI=", - "aarch64-linux": "sha256-kdh7Dpira7cx9lvtozFsjLrDqMBrUMcgxkhfsqrutws=", - "aarch64-darwin": "sha256-chPeMCX+8hvOQ32hTANIn7FmONsSv9t3dKTWTaHerjs=", - "x86_64-darwin": "sha256-rMc0dDZB+YbXrjYwXTIt7QvHUxSp//wDJQ5hUdR+LYM=" + "x86_64-linux": "sha256-Z/YgK5sf0L9MG7hMIb+wj3ias6iNo8L4oFHYNtRHreM=", + "aarch64-linux": "sha256-MoYZmeh6RcDUN7a132wexFNwkHz+Q9Ijs+qiacuKJ90=", + "aarch64-darwin": "sha256-OHo3x7UcJzrPMAF9Ij/9Rhamd+dfpb5QuxP89cy7Rw4=", + "x86_64-darwin": "sha256-ElzZVqkQPYTAd0nvLCB5uel9xtOqMK07QFW8MDr174E=" } } From 341c64cc97859719e401dd5edfacb6cca4a36970 Mon Sep 17 00:00:00 2001 From: Deven Navani <4031146+devennavani@users.noreply.github.com> Date: Wed, 29 Jul 2026 21:06:19 -0700 Subject: [PATCH 078/129] feat: discover Modal models (#39066) --- packages/opencode/src/plugin/index.ts | 2 + packages/opencode/src/plugin/modal/modal.ts | 17 ++ packages/opencode/src/plugin/modal/models.ts | 133 ++++++++++++ .../opencode/test/plugin/modal-models.test.ts | 198 ++++++++++++++++++ 4 files changed, 350 insertions(+) create mode 100644 packages/opencode/src/plugin/modal/modal.ts create mode 100644 packages/opencode/src/plugin/modal/models.ts create mode 100644 packages/opencode/test/plugin/modal-models.test.ts diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 1558c707c3..d68e8b34f9 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -13,6 +13,7 @@ import { CodexAuthPlugin } from "./openai/codex" import { Session } from "@/session/session" import { NamedError } from "@opencode-ai/core/util/error" import { CopilotAuthPlugin } from "./github-copilot/copilot" +import { ModalPlugin } from "./modal/modal" import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth" import { PoeAuthPlugin } from "opencode-poe-auth" import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare" @@ -70,6 +71,7 @@ function internalPlugins(flags: RuntimeFlags.Info): PluginInstance[] { experimentalWebSockets: experimentalWebSocketsEnabled({ enabled: flags.experimentalWebSockets }), }), CopilotAuthPlugin, + ModalPlugin, GitlabAuthPlugin, PoeAuthPlugin, CloudflareWorkersAuthPlugin, diff --git a/packages/opencode/src/plugin/modal/modal.ts b/packages/opencode/src/plugin/modal/modal.ts new file mode 100644 index 0000000000..942166e290 --- /dev/null +++ b/packages/opencode/src/plugin/modal/modal.ts @@ -0,0 +1,17 @@ +import type { Hooks } from "@opencode-ai/plugin" +import { ModalModels } from "./models" + +export async function ModalPlugin(): Promise { + return { + provider: { + id: "modal", + async models(provider, ctx) { + const apiKey = ctx.auth?.type === "api" ? ctx.auth.key : process.env.MODAL_PROXY_TOKEN + const baseURL = Object.values(provider.models)[0]?.api.url + if (!apiKey || !baseURL) return {} + + return ModalModels.get(baseURL, apiKey, provider.models).catch(() => ({})) + }, + }, + } +} diff --git a/packages/opencode/src/plugin/modal/models.ts b/packages/opencode/src/plugin/modal/models.ts new file mode 100644 index 0000000000..2f9d2b7e49 --- /dev/null +++ b/packages/opencode/src/plugin/modal/models.ts @@ -0,0 +1,133 @@ +import type { Model } from "@opencode-ai/sdk/v2" +import { Schema } from "effect" + +const reasoningOption = Schema.Struct({ + type: Schema.Literal("effort"), + values: Schema.Array(Schema.NullOr(Schema.String)), +}) + +const response = Schema.Struct({ + data: Schema.Array( + Schema.Struct({ + id: Schema.String, + base_model_id: Schema.optional(Schema.String), + hugging_face_id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + input_modalities: Schema.optional(Schema.Array(Schema.String)), + output_modalities: Schema.optional(Schema.Array(Schema.String)), + context_length: Schema.optional(Schema.Number), + max_output_length: Schema.optional(Schema.Number), + pricing: Schema.optional( + Schema.Struct({ + prompt: Schema.optional(Schema.Union([Schema.String, Schema.Number])), + completion: Schema.optional(Schema.Union([Schema.String, Schema.Number])), + input_cache_read: Schema.optional(Schema.Union([Schema.String, Schema.Number])), + }), + ), + supported_sampling_parameters: Schema.optional(Schema.Array(Schema.String)), + supported_features: Schema.optional(Schema.Array(Schema.String)), + reasoning_options: Schema.optional(Schema.Array(reasoningOption)), + interleaved: Schema.optional( + Schema.Union([ + Schema.Boolean, + Schema.Struct({ + field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]), + }), + ]), + ), + }), + ), +}) + +const decode = Schema.decodeUnknownSync(response) + +function price(value: string | number | undefined, fallback: number) { + if (value === undefined) return fallback + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed * 1_000_000 : fallback +} + +export async function get(baseURL: string, apiKey: string, existing: Record) { + const data = await fetch(`${baseURL.replace(/\/+$/, "")}/models`, { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + signal: AbortSignal.timeout(3_000), + }).then(async (res) => { + if (!res.ok) throw new Error(`Failed to fetch Modal models: ${res.status}`) + return decode(await res.json()) + }) + + return Object.fromEntries( + data.data.map((item) => { + const template = existing[item.base_model_id ?? item.hugging_face_id ?? item.id] + const model: Model = { + id: item.id, + providerID: "modal", + name: item.name ?? template?.name ?? item.id, + family: template?.family, + api: { + id: item.id, + url: baseURL, + npm: template?.api.npm ?? "@ai-sdk/openai-compatible", + }, + status: template?.status ?? "active", + headers: { ...template?.headers }, + options: { ...template?.options }, + cost: { + input: price(item.pricing?.prompt, template?.cost.input ?? 0), + output: price(item.pricing?.completion, template?.cost.output ?? 0), + cache: { + read: price(item.pricing?.input_cache_read, template?.cost.cache.read ?? 0), + write: template?.cost.cache.write ?? 0, + }, + }, + limit: { + context: item.context_length ?? template?.limit.context ?? 0, + input: template?.limit.input, + output: item.max_output_length ?? template?.limit.output ?? 0, + }, + capabilities: { + temperature: + item.supported_sampling_parameters?.includes("temperature") ?? template?.capabilities.temperature ?? false, + reasoning: item.supported_features?.includes("reasoning") ?? template?.capabilities.reasoning ?? false, + attachment: + item.input_modalities?.some((modality) => modality !== "text") ?? + template?.capabilities.attachment ?? + false, + toolcall: item.supported_features?.includes("tools") ?? template?.capabilities.toolcall ?? true, + input: { + text: item.input_modalities?.includes("text") ?? template?.capabilities.input.text ?? true, + audio: item.input_modalities?.includes("audio") ?? template?.capabilities.input.audio ?? false, + image: item.input_modalities?.includes("image") ?? template?.capabilities.input.image ?? false, + video: item.input_modalities?.includes("video") ?? template?.capabilities.input.video ?? false, + pdf: item.input_modalities?.includes("pdf") ?? template?.capabilities.input.pdf ?? false, + }, + output: { + text: item.output_modalities?.includes("text") ?? template?.capabilities.output.text ?? true, + audio: item.output_modalities?.includes("audio") ?? template?.capabilities.output.audio ?? false, + image: item.output_modalities?.includes("image") ?? template?.capabilities.output.image ?? false, + video: item.output_modalities?.includes("video") ?? template?.capabilities.output.video ?? false, + pdf: item.output_modalities?.includes("pdf") ?? template?.capabilities.output.pdf ?? false, + }, + interleaved: item.interleaved ?? template?.capabilities.interleaved ?? false, + }, + release_date: template?.release_date ?? "", + } + model.variants = + item.reasoning_options === undefined + ? template?.variants + : Object.fromEntries( + item.reasoning_options.flatMap((option) => + option.values.map((value) => { + const effort = value ?? "none" + return [effort, { reasoningEffort: effort }] + }), + ), + ) + return [item.id, model] + }), + ) +} + +export * as ModalModels from "./models" diff --git a/packages/opencode/test/plugin/modal-models.test.ts b/packages/opencode/test/plugin/modal-models.test.ts new file mode 100644 index 0000000000..1b37b31747 --- /dev/null +++ b/packages/opencode/test/plugin/modal-models.test.ts @@ -0,0 +1,198 @@ +import { expect, test } from "bun:test" +import type { Model, Provider } from "@opencode-ai/sdk/v2" +import { ModalPlugin } from "@/plugin/modal/modal" + +const BASE_MODEL_ID = "thinkingmachines/Inkling-NVFP4" +const RUNTIME_MODEL_ID = "workspace--inkling.us-west.modal.direct" +const FALLBACK_RUNTIME_MODEL_ID = "workspace--inkling-fallback.us-west.modal.direct" + +function makeProvider(baseURL: string): Provider { + const template: Model = { + id: BASE_MODEL_ID, + providerID: "modal", + name: "Inkling", + family: "ling", + api: { + id: BASE_MODEL_ID, + url: baseURL, + npm: "@ai-sdk/openai-compatible", + }, + status: "active", + headers: {}, + options: {}, + cost: { + input: 1, + output: 4, + cache: { + read: 0.2, + write: 0, + }, + }, + limit: { + context: 128_000, + output: 8_192, + }, + capabilities: { + temperature: true, + reasoning: true, + attachment: true, + toolcall: true, + input: { + text: true, + audio: true, + image: true, + video: false, + pdf: false, + }, + output: { + text: true, + audio: false, + image: false, + video: false, + pdf: false, + }, + interleaved: { + field: "reasoning_content", + }, + }, + release_date: "2026-07-15", + variants: { + fallback: { + reasoningEffort: "fallback", + }, + }, + } + + return { + id: "modal", + name: "Modal", + source: "api", + env: ["MODAL_PROXY_TOKEN"], + options: {}, + models: { + [template.id]: template, + }, + } +} + +test("discovers Modal workspace models", async () => { + const requests: Array<{ authorization: string | null; path: string }> = [] + using server = Bun.serve({ + port: 0, + fetch(request) { + requests.push({ + authorization: request.headers.get("authorization"), + path: new URL(request.url).pathname, + }) + return Response.json({ + data: [ + { + id: RUNTIME_MODEL_ID, + base_model_id: BASE_MODEL_ID, + name: "Thinking Machines: Inkling", + input_modalities: ["text", "image", "audio"], + output_modalities: ["text"], + context_length: 1_048_576, + max_output_length: 262_144, + pricing: { + prompt: "0.0000012", + completion: "0.000005", + input_cache_read: "0.00000027", + }, + supported_sampling_parameters: ["temperature"], + supported_features: ["tools", "reasoning"], + reasoning_options: [ + { + type: "effort", + values: ["none", "low", "medium", "high", "xhigh", "max"], + }, + ], + interleaved: { + field: "reasoning_content", + }, + }, + { + id: FALLBACK_RUNTIME_MODEL_ID, + base_model_id: BASE_MODEL_ID, + }, + ], + }) + }, + }) + const provider = makeProvider(`${server.url}v1`) + + const plugin = await ModalPlugin() + const models = await plugin.provider!.models!(provider, { + auth: { + type: "api", + key: "test-token", + }, + }) + const model = models[RUNTIME_MODEL_ID] + + expect(requests).toEqual([ + { + authorization: "Bearer test-token", + path: "/v1/models", + }, + ]) + expect(Object.keys(models)).toEqual([RUNTIME_MODEL_ID, FALLBACK_RUNTIME_MODEL_ID]) + expect(model.api).toEqual({ + id: RUNTIME_MODEL_ID, + url: `${server.url}v1`, + npm: "@ai-sdk/openai-compatible", + }) + expect(model.family).toBe("ling") + expect(model.capabilities.interleaved).toEqual({ field: "reasoning_content" }) + expect(model.capabilities.input).toEqual({ + text: true, + audio: true, + image: true, + video: false, + pdf: false, + }) + expect(model.cost).toEqual({ + input: 1.2, + output: 5, + cache: { + read: 0.27, + write: 0, + }, + }) + expect(model.limit).toEqual({ + context: 1_048_576, + output: 262_144, + }) + expect(model.variants).toEqual({ + none: { reasoningEffort: "none" }, + low: { reasoningEffort: "low" }, + medium: { reasoningEffort: "medium" }, + high: { reasoningEffort: "high" }, + xhigh: { reasoningEffort: "xhigh" }, + max: { reasoningEffort: "max" }, + }) + expect(models[FALLBACK_RUNTIME_MODEL_ID].variants).toEqual({ + fallback: { + reasoningEffort: "fallback", + }, + }) +}) + +test("hides Modal models when discovery fails", async () => { + using server = Bun.serve({ + port: 0, + fetch() { + return new Response(null, { status: 503 }) + }, + }) + + const plugin = await ModalPlugin() + const models = await plugin.provider!.models!(makeProvider(`${server.url}v1`), { + auth: { + type: "api", + key: "test-token", + }, + }) + + expect(models).toEqual({}) +}) From f7204902192624481e526b1852cdedaadb914e24 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 30 Jul 2026 00:40:47 -0400 Subject: [PATCH 079/129] fix(console): add public fetch compatibility flag --- infra/console.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/infra/console.ts b/infra/console.ts index 5fbf35de00..79556f5e0c 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -288,8 +288,12 @@ new sst.cloudflare.x.SolidStart("Console", { server: { placement: { region: "aws:us-east-2" }, transform: { - worker: { - tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }], + worker: (args) => { + args.compatibilityFlags = $resolve(args.compatibilityFlags).apply((flags) => [ + ...(flags ?? []), + "global_fetch_strictly_public", + ]) + args.tailConsumers = [{ service: logProcessor.nodes.worker.scriptName }] }, }, }, From 73009f0b09d866f2efe1ccc9fad91a7430345f7d Mon Sep 17 00:00:00 2001 From: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:26:16 +0530 Subject: [PATCH 080/129] feat(app): update arrow buttons in review tab top bar (#39469) Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> --- .../src/v2/components/session-review-v2.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/session-ui/src/v2/components/session-review-v2.tsx b/packages/session-ui/src/v2/components/session-review-v2.tsx index abbe90d112..bfd6778c54 100644 --- a/packages/session-ui/src/v2/components/session-review-v2.tsx +++ b/packages/session-ui/src/v2/components/session-review-v2.tsx @@ -165,11 +165,13 @@ export function SessionReviewV2(props: SessionReviewV2Props) { } const prev = () => { - return props.files[fileIndex() - 1] + if (!canCycle()) return + return props.files[(fileIndex() - 1 + props.files.length) % props.files.length] } const next = () => { - return props.files[fileIndex() + 1] + if (!canCycle()) return + return props.files[(fileIndex() + 1) % props.files.length] } const canCycle = () => props.files.length > 0 @@ -183,15 +185,15 @@ export function SessionReviewV2(props: SessionReviewV2Props) { props.onSelectFile(file) } - // The prev/next tooltips advertise < and >; keep the keys working while the + // Keep the advertised arrow keys working while the // pane is mounted, but never while typing in an input or comment editor. makeEventListener(document, "keydown", (event) => { if (event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) return - if (event.key !== "<" && event.key !== ">") return + if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return const target = event.target if (target instanceof HTMLElement && (target.isContentEditable || target.closest("input, textarea, select"))) return if (!props.hasDiffs || !canCycle()) return - const file = event.key === "<" ? prev() : next() + const file = event.key === "ArrowLeft" ? prev() : next() if (!file) return event.preventDefault() cycle(file) @@ -219,7 +221,7 @@ export function SessionReviewV2(props: SessionReviewV2Props) { value={ <> {i18n.t("ui.sessionReviewV2.previousFile")} - + } > @@ -239,7 +241,7 @@ export function SessionReviewV2(props: SessionReviewV2Props) { value={ <> {i18n.t("ui.sessionReviewV2.nextFile")} - "]} variant="neutral" /> + } > From a9eda2e99e8ade65f735aadb184685d704dc18ea Mon Sep 17 00:00:00 2001 From: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:27:19 +0530 Subject: [PATCH 081/129] feat(app): migrate to solid-sonner (#39519) Co-authored-by: Brendan Allan --- bun.lock | 4 + package.json | 1 + packages/app/package.json | 4 +- packages/app/src/pages/layout.tsx | 9 +- packages/app/src/utils/toast.tsx | 17 +- packages/app/test-browser/toast-owner.test.ts | 40 ++- packages/ui/package.json | 1 + packages/ui/src/v2/components/toast-v2.css | 336 +++++++++--------- .../ui/src/v2/components/toast-v2.stories.tsx | 8 +- packages/ui/src/v2/components/toast-v2.tsx | 277 +++++++++++---- 10 files changed, 447 insertions(+), 250 deletions(-) diff --git a/bun.lock b/bun.lock index aaf43212b7..9f6ef7b628 100644 --- a/bun.lock +++ b/bun.lock @@ -1002,6 +1002,7 @@ "remend": "catalog:", "shiki": "catalog:", "solid-list": "catalog:", + "solid-sonner": "catalog:", "strip-ansi": "7.1.2", }, "devDependencies": { @@ -1146,6 +1147,7 @@ "shiki": "4.2.0", "solid-js": "1.9.10", "solid-list": "0.3.0", + "solid-sonner": "0.3.1", "sst": "4.13.1", "tailwindcss": "4.1.11", "typescript": "5.8.2", @@ -5127,6 +5129,8 @@ "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], + "solid-sonner": ["solid-sonner@0.3.1", "", { "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-F/+zi9yKJTHh5hX1UGJfkDvyC+F34Vi3jgy44NJwOKCgic1QtAon0b1iT9OsDO77RTgR+PCil+3Y5B8T2Owy1Q=="], + "solid-stripe": ["solid-stripe@0.8.1", "", { "peerDependencies": { "@stripe/stripe-js": ">=1.44.1 <8.0.0", "solid-js": "^1.6.0" } }, "sha512-l2SkWoe51rsvk9u1ILBRWyCHODZebChSGMR6zHYJTivTRC0XWrRnNNKs5x1PYXsaIU71KYI6ov5CZB5cOtGLWw=="], "solid-transition-size": ["solid-transition-size@0.1.4", "", { "dependencies": { "@corvu/utils": "~0.3.2" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-ocHVnbfy23CgfaH4cEUR/AFg0Y3CEL8Oh3n9Qv8OHFJgPh+zkmERKZQfi/xH5XvxDCizg8VjPrVUhiHB1Gza8g=="], diff --git a/package.json b/package.json index e479eb7406..2a8eef6356 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "@sentry/solid": "10.36.0", "@sentry/vite-plugin": "4.6.0", "solid-js": "1.9.10", + "solid-sonner": "0.3.1", "vite-plugin-solid": "2.11.10", "@lydell/node-pty": "1.2.0-beta.12" } diff --git a/packages/app/package.json b/packages/app/package.json index 7199ad9425..08eb9a059f 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -19,9 +19,9 @@ "build": "vite build", "serve": "vite preview", "test": "bun run test:unit && bun run test:browser", - "test:unit": "bun test --only-failures --preload ./happydom.ts ./src", + "test:unit": "bun test --conditions=solid --only-failures --preload ./happydom.ts ./src", "test:browser": "bun test --conditions=browser --preload ./happydom.ts ./test-browser", - "test:unit:watch": "bun test --watch --preload ./happydom.ts ./src", + "test:unit:watch": "bun test --conditions=solid --watch --preload ./happydom.ts ./src", "test:e2e": "playwright test", "test:e2e:local": "playwright test", "test:e2e:ui": "playwright test --ui", diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index b348b44d8a..694b610903 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -33,8 +33,7 @@ import { createStore, produce, reconcile } from "solid-js/store" import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" import type { DragEvent } from "@thisbeyond/solid-dnd" import { useProviders } from "@/hooks/use-providers" -import { toaster } from "@opencode-ai/ui/toast" -import { setV2Toast, showToast, ToastRegion } from "@/utils/toast" +import { dismissToast, setV2Toast, showToast, ToastRegion } from "@/utils/toast" import { useServerSDK } from "@/context/server-sdk" import { normalizeProjectInfo } from "@/context/global-sync/utils" import { clearWorkspaceTerminals } from "@/context/terminal" @@ -384,7 +383,7 @@ export default function LegacyLayout(props: ParentProps) { const dismissSessionAlert = (sessionKey: string) => { const toastId = toastBySession.get(sessionKey) if (toastId === undefined) return - toaster.dismiss(toastId) + dismissToast(toastId) toastBySession.delete(sessionKey) alertedAtBySession.delete(sessionKey) } @@ -1459,7 +1458,7 @@ export default function LegacyLayout(props: ParentProps) { title: language.t("workspace.resetting.title"), description: language.t("workspace.resetting.description"), }) - const dismiss = () => toaster.dismiss(progress) + const dismiss = () => dismissToast(progress) const sessions = await listAllSessions(serverSDK().api.session, { directory, order: "desc" }).catch(() => []) @@ -2450,7 +2449,7 @@ function UpdateAvailableToast(props: { onCleanup(() => { if (toastId === undefined) return - toaster.dismiss(toastId) + dismissToast(toastId) }) return null diff --git a/packages/app/src/utils/toast.tsx b/packages/app/src/utils/toast.tsx index e444548508..6f23b63d1e 100644 --- a/packages/app/src/utils/toast.tsx +++ b/packages/app/src/utils/toast.tsx @@ -1,6 +1,12 @@ import { Icon, type IconProps } from "@opencode-ai/ui/icon" -import { Toast, showToast as showLegacyToast, type ToastOptions, type ToastVariant } from "@opencode-ai/ui/toast" -import { ToastV2, showToastV2 } from "@opencode-ai/ui/v2/toast-v2" +import { + Toast, + showToast as showLegacyToast, + toaster as legacyToaster, + type ToastOptions, + type ToastVariant, +} from "@opencode-ai/ui/toast" +import { ToastV2, showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2" let v2 = false @@ -27,6 +33,13 @@ export function showToast(options: ToastOptions | string) { }) } +// v1 and v2 ids come from separate registries, so dismissal has to use the same +// implementation that issued the id. +export function dismissToast(toastId: number) { + if (!v2) return legacyToaster.dismiss(toastId) + return toasterV2.dismiss(toastId) +} + function resolveIcon(icon: IconProps["name"] | undefined, variant: ToastVariant | undefined) { const name = icon ?? (variant === "success" ? "check" : undefined) if (!name) return diff --git a/packages/app/test-browser/toast-owner.test.ts b/packages/app/test-browser/toast-owner.test.ts index 104a505851..25ba5c000e 100644 --- a/packages/app/test-browser/toast-owner.test.ts +++ b/packages/app/test-browser/toast-owner.test.ts @@ -1,8 +1,46 @@ -import { describe, expect, test } from "bun:test" +import { beforeEach, describe, expect, test } from "bun:test" import { createSignal, type JSX } from "solid-js" import { showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2" describe("showToastV2", () => { + // The toast registry is module state, so each test starts from an empty stack. + beforeEach(() => { + toasterV2.dismiss() + }) + + test("coalesces exact active content", () => { + const first = showToastV2({ title: "Repeated error", description: "Try again" }) + const second = showToastV2({ title: "Repeated error", description: "Try again" }) + const different = showToastV2({ title: "Repeated error", description: "A different error" }) + + expect(second).toBe(first) + expect(different).not.toBe(first) + + toasterV2.dismiss(first) + toasterV2.dismiss(different) + }) + + test("allows dismissed content to appear again", () => { + const first = showToastV2("Dismiss and retry") + toasterV2.dismiss(first) + + const second = showToastV2("Dismiss and retry") + expect(second).not.toBe(first) + + toasterV2.dismiss(second) + }) + + test("recreates matching content when it is not the topmost toast", () => { + const first = showToastV2("First toast") + const topmost = showToastV2("Topmost toast") + const repeated = showToastV2("First toast") + + expect(repeated).not.toBe(first) + + toasterV2.dismiss(topmost) + toasterV2.dismiss(repeated) + }) + test("creates no reactive computations at call time", () => { const [tick, setTick] = createSignal(0) let reads = 0 diff --git a/packages/ui/package.json b/packages/ui/package.json index 05c4ffbf63..f994110b9f 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -104,6 +104,7 @@ "remend": "catalog:", "shiki": "catalog:", "solid-list": "catalog:", + "solid-sonner": "catalog:", "strip-ansi": "7.1.2" }, "peerDependencies": { diff --git a/packages/ui/src/v2/components/toast-v2.css b/packages/ui/src/v2/components/toast-v2.css index 5bce87dbdc..db173489a9 100644 --- a/packages/ui/src/v2/components/toast-v2.css +++ b/packages/ui/src/v2/components/toast-v2.css @@ -1,58 +1,182 @@ -[data-component="toast-v2-region"] { - position: fixed; - bottom: 48px; - right: 32px; +.toast-v2-region { z-index: 1000; - display: flex; - flex-direction: column; - gap: 12px; max-width: min(320px, calc(100vw - 64px)); - max-height: none; - width: 100%; - overflow: visible; pointer-events: none; - - [data-slot="toast-v2-list"] { - display: flex; - flex-direction: column; - gap: 12px; - list-style: none; - margin: 0; - padding: 0; - max-height: none; - overflow-y: visible; - overflow-x: visible; - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } - } + user-select: none; + -webkit-user-select: none; } -[data-component="toast-v2"] { - position: relative; - display: flex; - flex-direction: column; - gap: 12px; +.toast-v2 { + display: grid; + grid-template-columns: minmax(0, 1fr) 20px; + column-gap: 12px; + row-gap: 12px; width: 320px; + max-width: 100%; padding: 12px; max-height: min(420px, calc(100dvh - 96px)); overflow: hidden; pointer-events: auto; - transition: transform 140ms ease-out; - + box-sizing: border-box; + outline: none; + user-select: none; + -webkit-user-select: none; border-radius: 8px; color: var(--v2-text-text-base); background: var(--v2-background-bg-layer-01); box-shadow: var(--v2-elevation-floating); + transition: + transform 280ms cubic-bezier(0.2, 0, 0, 1), + opacity 160ms ease-out, + height 280ms cubic-bezier(0.2, 0, 0, 1), + box-shadow 160ms ease-out; - &[data-opened] { - animation: toastV2PopIn 140ms ease-out; + &:has(> [data-icon]) { + grid-template-columns: 16px minmax(0, 1fr) 20px; } - &[data-closed] { - animation: toastV2PopOut 100ms ease-in forwards; + &:focus-visible { + box-shadow: + var(--v2-elevation-floating), + 0 0 0 2px var(--v2-border-border-focus); + } + + &[data-expanded="true"] { + overflow: visible; + } + + &[data-expanded="false"][data-front="false"] > * { + opacity: 0; + } + + &[data-expanded="false"][data-front="false"] { + --y: translateY(calc(-1 * var(--toasts-before) * 9.5px)) scale(calc(1 - var(--toasts-before) * 0.05)); + } + + > [data-icon] { + grid-column: 1; + grid-row: 1; + align-self: start; + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 20px; + color: var(--v2-icon-icon-base); + } + + > [data-content] { + grid-column: 1; + grid-row: 1; + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; + overflow: hidden; + } + + &:has(> [data-icon]) > [data-content] { + grid-column: 2; + } + + > [data-close-button] { + grid-column: 2; + grid-row: 1; + position: static; + align-self: start; + width: 20px; + height: 20px; + min-width: 20px; + min-height: 20px; + padding: 0; + border: 0; + border-radius: 4px; + transform: none; + background: transparent; + color: var(--v2-icon-icon-muted); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + } + + &:has(> [data-icon]) > [data-close-button] { + grid-column: 3; + } + + > [data-close-button]:hover { + background: var(--v2-overlay-simple-overlay-hover); + color: var(--v2-icon-icon-base); + } + + > [data-close-button]:active { + background: var(--v2-overlay-simple-overlay-pressed); + } + + > [data-close-button]:focus-visible { + outline: 2px solid var(--v2-border-border-focus); + outline-offset: 2px; + } + + > [data-icon] > *, + > [data-icon] svg, + > [data-close-button] svg { + width: 16px; + height: 16px; + display: block; + flex-shrink: 0; + } + + [data-title], + [data-slot="toast-v2-title"] { + color: var(--v2-text-text-base); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 13px; + font-style: normal; + font-weight: 530; + line-height: 20px; + letter-spacing: -0.04px; + font-synthesis: none; + margin: 0; + } + + [data-description], + [data-slot="toast-v2-description"] { + color: var(--v2-text-text-muted); + text-wrap-style: pretty; + overflow-wrap: anywhere; + word-break: break-word; + font-size: 13px; + font-style: normal; + font-weight: 440; + line-height: 20px; + letter-spacing: -0.04px; + font-synthesis: none; + margin: 0; + } + + [data-slot="toast-v2-actions"] { + grid-column: 1; + grid-row: 2; + display: flex; + flex-wrap: wrap; + gap: 8px; + } + + &:has(> [data-icon]) [data-slot="toast-v2-actions"] { + grid-column: 2; + } + + [data-slot="toast-v2-actions"] [data-component="button-v2"] { + min-height: 24px; + font-size: 13px; + font-style: normal; + font-weight: 530; + line-height: 20px; + letter-spacing: -0.04px; + font-synthesis: none; } [data-slot="toast-v2-header"] { @@ -69,34 +193,7 @@ justify-content: center; width: 16px; height: 20px; - min-width: 16px; - min-height: 20px; color: var(--v2-icon-icon-base); - - [data-component="icon"] { - color: var(--v2-icon-icon-base); - width: 16px; - height: 16px; - display: inline-flex; - align-items: center; - justify-content: center; - } - - > * { - width: 16px; - height: 16px; - display: inline-flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - } - - svg { - width: 16px; - height: 16px; - display: block; - flex-shrink: 0; - } } [data-slot="toast-v2-content"] { @@ -104,112 +201,25 @@ display: flex; flex-direction: column; gap: 4px; - min-height: 0; min-width: 0; overflow: hidden; } +} - [data-slot="toast-v2-title"] { - color: var(--v2-text-text-base); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 13px; - font-style: normal; - font-weight: 530; - line-height: 20px; - letter-spacing: -0.04px; - font-synthesis: none; - margin: 0; - } - - [data-slot="toast-v2-description"] { - color: var(--v2-text-text-muted); - text-wrap-style: pretty; - overflow-wrap: anywhere; - word-break: break-word; - font-size: 13px; - font-style: normal; - font-weight: 440; - line-height: 20px; - letter-spacing: -0.04px; - font-synthesis: none; - margin: 0; - } - - [data-slot="toast-v2-actions"] { - display: flex; - flex-wrap: wrap; - gap: 8px; - padding-left: 28px; - } - - [data-slot="toast-v2-actions"] [data-component="button-v2"] { - min-height: 24px; - font-size: 13px; - font-style: normal; - font-weight: 530; - line-height: 20px; - letter-spacing: -0.04px; - font-synthesis: none; - } - - [data-slot="toast-v2-close-button"] { - flex-shrink: 0; - width: 20px; - height: 20px; - min-width: 20px; - min-height: 20px; - padding: 0; - border: 0; - border-radius: 4px; - background: transparent; - color: var(--v2-icon-icon-muted); - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - - &:hover { - background: var(--v2-overlay-simple-overlay-hover); - color: var(--v2-icon-icon-base); - } - - &:active { - background: var(--v2-overlay-simple-overlay-pressed); - } - - &:focus-visible { - outline: 2px solid var(--v2-border-border-focus); - outline-offset: 2px; - } - - svg { - width: 16px; - height: 16px; - display: block; - } +@media (prefers-reduced-motion: reduce) { + .toast-v2, + .toast-v2 > * { + animation: none !important; + transition: none !important; } } -@keyframes toastV2PopIn { - from { - opacity: 0.8; - transform: translateY(6px); +@media (max-width: 600px) { + .toast-v2-region { + max-width: calc(100vw - 32px); } - to { - transform: translateY(0); - opacity: 1; - } -} -@keyframes toastV2PopOut { - from { - transform: translateY(0); - opacity: 1; - } - to { - transform: translateY(6px); - opacity: 0.8; + .toast-v2 { + width: 100%; } } diff --git a/packages/ui/src/v2/components/toast-v2.stories.tsx b/packages/ui/src/v2/components/toast-v2.stories.tsx index f096459739..b2af8c038d 100644 --- a/packages/ui/src/v2/components/toast-v2.stories.tsx +++ b/packages/ui/src/v2/components/toast-v2.stories.tsx @@ -19,7 +19,7 @@ Use brief titles/descriptions; limit actions to 1-2. - Toasts render in a portal and auto-dismiss unless persistent. ### Accessibility -- TODO: confirm aria-live behavior from Kobalte Toast. +- The region announces additions through an ARIA live region and expands with Alt+T. ### Theming/tokens - Uses \`data-component="toast-v2"\` and slot data attributes. @@ -57,10 +57,10 @@ export const AllExamples = { - - + + ), actions: [ diff --git a/packages/ui/src/v2/components/toast-v2.tsx b/packages/ui/src/v2/components/toast-v2.tsx index d2dc1b3605..efb6ecd40c 100644 --- a/packages/ui/src/v2/components/toast-v2.tsx +++ b/packages/ui/src/v2/components/toast-v2.tsx @@ -1,40 +1,78 @@ -import { Toast as Kobalte, toaster } from "@kobalte/core/toast" -import type { ToastRootProps, ToastCloseButtonProps, ToastTitleProps, ToastDescriptionProps } from "@kobalte/core/toast" +import { Toaster, toast, type ToasterProps } from "solid-sonner" import type { ComponentProps, JSX } from "solid-js" -import { Show, children } from "solid-js" +import { createContext, onCleanup, onMount, splitProps, useContext } from "solid-js" import { Portal } from "solid-js/web" -import { ButtonV2 } from "./button-v2" +import "./button-v2.css" import "./toast-v2.css" -export interface ToastV2RegionProps extends ComponentProps {} +export interface ToastV2RegionProps extends ToasterProps {} function ToastV2Region(props: ToastV2RegionProps) { + const [local, rest] = splitProps(props, ["class", "className", "style", "toastOptions", "swipeDirections"]) + onMount(() => { + const sync = () => { + document.querySelectorAll(".toast-v2-region .toast-v2").forEach((element) => { + const hidden = element.dataset.visible === "false" + element.inert = hidden + element.tabIndex = hidden ? -1 : 0 + }) + } + let connected = false + const connect = () => { + const regions = document.querySelectorAll(".toast-v2-region") + if (!regions.length) return + observer.disconnect() + regions.forEach((region) => { + observer.observe(region, { + subtree: true, + childList: true, + attributes: true, + attributeFilter: ["data-visible"], + }) + }) + connected = true + sync() + } + const observer = new MutationObserver(() => { + if (!connected) connect() + else sync() + }) + observer.observe(document.body, { subtree: true, childList: true }) + queueMicrotask(connect) + onCleanup(() => observer.disconnect()) + }) return ( - - - + ) } -export interface ToastV2RootComponentProps extends ToastRootProps { - class?: string - classList?: ComponentProps<"li">["classList"] +const ToastV2Context = createContext() + +export interface ToastV2RootComponentProps { + toastId: number children?: JSX.Element } function ToastV2Root(props: ToastV2RootComponentProps) { - return ( - - ) + return {props.children} } function ToastV2Icon(props: ComponentProps<"div">) { @@ -45,26 +83,43 @@ function ToastV2Content(props: ComponentProps<"div">) { return
} -function ToastV2Title(props: ToastTitleProps & ComponentProps<"div">) { - return +function ToastV2Title(props: ComponentProps<"div">) { + return
} -function ToastV2Description(props: ToastDescriptionProps & ComponentProps<"div">) { - return +function ToastV2Description(props: ComponentProps<"div">) { + return
} function ToastV2Actions(props: ComponentProps<"div">) { return
} -function ToastV2CloseButton(props: ToastCloseButtonProps & ComponentProps<"button">) { +function ToastV2CloseButton(props: ComponentProps<"button">) { + const toastId = useContext(ToastV2Context) + const [local, rest] = splitProps(props, ["children", "onClick"]) return ( - - - + + ) +} + +function CloseIcon() { + return ( + ) } @@ -78,7 +133,29 @@ export const ToastV2 = Object.assign(ToastV2Root, { CloseButton: ToastV2CloseButton, }) -export { toaster as toasterV2 } +let toastV2Id = 0 + +export const toasterV2 = { + show(render: (props: { toastId: number }) => JSX.Element, options?: { duration?: number; persistent?: boolean }) { + const toastId = --toastV2Id + toast.custom((id) => render({ toastId: Number(id) }), { + id: toastId, + className: "toast-v2", + duration: options?.persistent ? Number.POSITIVE_INFINITY : options?.duration, + unstyled: true, + }) + return toastId + }, + dismiss(toastId?: number) { + if (toastId === undefined) { + activeToastV2ByKey.clear() + activeToastV2ById.clear() + } else { + releaseToastV2(activeToastV2ById.get(toastId)) + } + return toast.dismiss(toastId) + }, +} export interface ToastV2Action { label: string @@ -90,53 +167,107 @@ export interface ToastV2Options { title?: string description?: string icon?: JSX.Element + variant?: "default" | "success" | "error" | "loading" duration?: number persistent?: boolean actions?: ToastV2Action[] } +interface ActiveToastV2 { + id: number + key: string + options: ToastV2Options + actions?: JSX.Element +} + +const activeToastV2ByKey = new Map() +const activeToastV2ById = new Map() + export function showToastV2(options: ToastV2Options | string) { - const opts = typeof options === "string" ? { description: options } : options - return toaster.show((props) => { - const resolvedIcon = children(() => opts.icon) - return ( - -
- - {resolvedIcon()} - - - - {opts.title} - - - {opts.description} - - - -
- - - {opts.actions!.map((action) => ( - { - if (typeof action.onClick === "function") { - action.onClick() - } - toaster.dismiss(props.toastId) - }} - > - {action.label} - - ))} - - -
- ) + const opts: ToastV2Options = typeof options === "string" ? { description: options } : options + const key = JSON.stringify({ + title: opts.title, + description: opts.description, + variant: opts.variant, + duration: opts.duration, + persistent: opts.persistent, + actions: opts.actions?.map((action) => [action.label, action.variant]), }) + const active = activeToastV2ByKey.get(key) + const toasts = toast.getToasts() + + if (active && toasts.at(-1)?.id === active.id) { + active.options = opts + publishToastV2(active) + pulseToastV2(active.id) + return active.id + } + + if (active && toasts.some((item) => item.id === active.id)) toasterV2.dismiss(active.id) + releaseToastV2(active) + + const entry: ActiveToastV2 = { id: --toastV2Id, key, options: opts } + entry.actions = createToastV2Actions(entry) + activeToastV2ByKey.set(key, entry) + activeToastV2ById.set(entry.id, entry) + publishToastV2(entry) + return entry.id +} + +function publishToastV2(entry: ActiveToastV2) { + const release = () => releaseToastV2(entry) + toast(entry.options.title ?? "", { + id: entry.id, + description: entry.options.description, + icon: entry.options.icon, + action: entry.actions, + closeButton: true, + duration: entry.options.persistent ? Number.POSITIVE_INFINITY : entry.options.duration, + className: `toast-v2 toast-v2--${entry.options.variant ?? "default"}`, + testId: `toast-v2-${entry.id}`, + unstyled: true, + onDismiss: release, + onAutoClose: release, + }) +} + +function createToastV2Actions(entry: ActiveToastV2) { + if (!entry.options.actions?.length) return undefined + return ( + + {entry.options.actions.map((action, index) => ( + + ))} + + ) +} + +function pulseToastV2(toastId: number) { + if (typeof document === "undefined" || typeof requestAnimationFrame === "undefined") return + requestAnimationFrame(() => { + const element = document.querySelector(`[data-testid="toast-v2-${toastId}"]`) + if (!element || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return + element.animate([{ scale: 1 }, { scale: 1.025 }, { scale: 1 }], { duration: 160, easing: "ease-out" }) + }) +} + +function releaseToastV2(entry: ActiveToastV2 | undefined) { + if (!entry) return + if (activeToastV2ByKey.get(entry.key) === entry) activeToastV2ByKey.delete(entry.key) + if (activeToastV2ById.get(entry.id) === entry) activeToastV2ById.delete(entry.id) } export interface ToastV2PromiseOptions { From 5b11635d97ecc5dceb66752c876b4cd6c5da8043 Mon Sep 17 00:00:00 2001 From: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:28:38 +0530 Subject: [PATCH 082/129] feat(app): always show new session button (#39520) Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> --- packages/app/src/components/titlebar.tsx | 40 +++++++++++------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 32d7e80b2f..649a8f5777 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -395,27 +395,25 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl }} onReorder={(keys) => tabsStoreActions.reorder(keys)} /> - - - {language.t("command.session.new")} - - - } - > - } - onClick={openNewTab} - aria-label={language.t("command.session.new")} - /> - - + + {language.t("command.session.new")} + + + } + > + } + onClick={openNewTab} + aria-label={language.t("command.session.new")} + /> +
From eca5e68a5ea7f2f54ec0d81a46a41110c43e62b4 Mon Sep 17 00:00:00 2001 From: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:32:15 +0530 Subject: [PATCH 083/129] feat(app): disallow duplicate attachments (#39464) --- .../app/src/components/prompt-input-v2.tsx | 1 + packages/app/src/i18n/ar.ts | 1 + packages/app/src/i18n/br.ts | 1 + packages/app/src/i18n/bs.ts | 1 + packages/app/src/i18n/da.ts | 1 + packages/app/src/i18n/de.ts | 1 + packages/app/src/i18n/en.ts | 1 + packages/app/src/i18n/es.ts | 1 + packages/app/src/i18n/fr.ts | 1 + packages/app/src/i18n/ja.ts | 1 + packages/app/src/i18n/ko.ts | 1 + packages/app/src/i18n/no.ts | 1 + packages/app/src/i18n/pl.ts | 1 + packages/app/src/i18n/ru.ts | 1 + packages/app/src/i18n/th.ts | 1 + packages/app/src/i18n/tr.ts | 1 + packages/app/src/i18n/uk.ts | 1 + packages/app/src/i18n/zh.ts | 1 + packages/app/src/i18n/zht.ts | 1 + .../test-browser/prompt-attachments.test.ts | 86 +++++++++++++++++++ .../v2/components/prompt-input/attachments.ts | 22 ++++- 21 files changed, 124 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/prompt-input-v2.tsx b/packages/app/src/components/prompt-input-v2.tsx index 634e90f78f..2d6a5e98d6 100644 --- a/packages/app/src/components/prompt-input-v2.tsx +++ b/packages/app/src/components/prompt-input-v2.tsx @@ -368,6 +368,7 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): title: language.t("prompt.toast.pasteUnsupported.title"), description: language.t("prompt.toast.pasteUnsupported.description"), }), + duplicate: () => showToast({ title: language.t("prompt.toast.attachmentDuplicate.title") }), onError: (error) => showToast({ variant: "error", diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 320738a6b0..89fa394637 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -277,6 +277,7 @@ export const dict = { "prompt.action.send": "إرسال", "prompt.action.stop": "توقف", "prompt.toast.pasteUnsupported.title": "مرفق غير مدعوم", + "prompt.toast.attachmentDuplicate.title": "تم تحميل هذا الملف بالفعل", "prompt.toast.pasteUnsupported.description": "يمكن إرفاق الصور أو ملفات PDF أو الملفات النصية فقط هنا.", "prompt.toast.modelAgentRequired.title": "حدد وكيلاً ونموذجاً", "prompt.toast.modelAgentRequired.description": "اختر وكيلاً ونموذجاً قبل إرسال الموجه.", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 9d6765ec94..f6e5d8df9d 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -277,6 +277,7 @@ export const dict = { "prompt.action.send": "Enviar", "prompt.action.stop": "Parar", "prompt.toast.pasteUnsupported.title": "Anexo não suportado", + "prompt.toast.attachmentDuplicate.title": "Este arquivo já foi enviado", "prompt.toast.pasteUnsupported.description": "Apenas imagens, PDFs ou arquivos de texto podem ser anexados aqui.", "prompt.toast.modelAgentRequired.title": "Selecione um agente e modelo", "prompt.toast.modelAgentRequired.description": "Escolha um agente e modelo antes de enviar um prompt.", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 915313c3e2..3343df7a2b 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -298,6 +298,7 @@ export const dict = { "prompt.action.stop": "Zaustavi", "prompt.toast.pasteUnsupported.title": "Nepodržan prilog", + "prompt.toast.attachmentDuplicate.title": "Ova datoteka je već učitana", "prompt.toast.pasteUnsupported.description": "Ovdje se mogu priložiti samo slike, PDF-ovi ili tekstualne datoteke.", "prompt.toast.modelAgentRequired.title": "Odaberi agenta i model", "prompt.toast.modelAgentRequired.description": "Odaberi agenta i model prije slanja upita.", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 22fcdf39ce..52bfb642c3 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -296,6 +296,7 @@ export const dict = { "prompt.action.stop": "Stop", "prompt.toast.pasteUnsupported.title": "Ikke understøttet vedhæftning", + "prompt.toast.attachmentDuplicate.title": "Denne fil er allerede uploadet", "prompt.toast.pasteUnsupported.description": "Kun billeder, PDF'er eller tekstfiler kan vedhæftes her.", "prompt.toast.modelAgentRequired.title": "Vælg en agent og model", "prompt.toast.modelAgentRequired.description": "Vælg en agent og model før du sender en forespørgsel.", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index bc2ac9be38..8619546079 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -282,6 +282,7 @@ export const dict = { "prompt.action.send": "Senden", "prompt.action.stop": "Stopp", "prompt.toast.pasteUnsupported.title": "Nicht unterstützter Anhang", + "prompt.toast.attachmentDuplicate.title": "Diese Datei wurde bereits hochgeladen", "prompt.toast.pasteUnsupported.description": "Hier können nur Bilder, PDFs oder Textdateien angehängt werden.", "prompt.toast.modelAgentRequired.title": "Wählen Sie einen Agenten und ein Modell", "prompt.toast.modelAgentRequired.description": diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index f03147f151..76623f4468 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -301,6 +301,7 @@ export const dict = { "prompt.toast.pasteUnsupported.title": "Unsupported attachment", "prompt.toast.pasteUnsupported.description": "Only images, PDFs, or text files can be attached here.", + "prompt.toast.attachmentDuplicate.title": "This file has already been uploaded", "prompt.toast.modelAgentRequired.title": "Select an agent and model", "prompt.toast.modelAgentRequired.description": "Choose an agent and model before sending a prompt.", "prompt.toast.worktreeCreateFailed.title": "Failed to create worktree", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 00dca6704b..043c997c04 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -297,6 +297,7 @@ export const dict = { "prompt.action.stop": "Detener", "prompt.toast.pasteUnsupported.title": "Adjunto no compatible", + "prompt.toast.attachmentDuplicate.title": "Este archivo ya se ha subido", "prompt.toast.pasteUnsupported.description": "Solo se pueden adjuntar imágenes, PDFs o archivos de texto aquí.", "prompt.toast.modelAgentRequired.title": "Selecciona un agente y modelo", "prompt.toast.modelAgentRequired.description": "Elige un agente y modelo antes de enviar un prompt.", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index d1487ff61e..fc5c5bbb34 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -277,6 +277,7 @@ export const dict = { "prompt.action.send": "Envoyer", "prompt.action.stop": "Arrêter", "prompt.toast.pasteUnsupported.title": "Pièce jointe non prise en charge", + "prompt.toast.attachmentDuplicate.title": "Ce fichier a déjà été téléversé", "prompt.toast.pasteUnsupported.description": "Seules les images, les PDF ou les fichiers texte peuvent être joints ici.", "prompt.toast.modelAgentRequired.title": "Sélectionnez un agent et un modèle", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 6a3b1ebcb0..56c63e0e70 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -276,6 +276,7 @@ export const dict = { "prompt.action.send": "送信", "prompt.action.stop": "停止", "prompt.toast.pasteUnsupported.title": "サポートされていない添付ファイル", + "prompt.toast.attachmentDuplicate.title": "このファイルはすでにアップロードされています", "prompt.toast.pasteUnsupported.description": "画像、PDF、またはテキストファイルのみ添付できます。", "prompt.toast.modelAgentRequired.title": "エージェントとモデルを選択", "prompt.toast.modelAgentRequired.description": "プロンプトを送信する前にエージェントとモデルを選択してください。", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 3c262ce3b9..02d9a5e0e1 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -264,6 +264,7 @@ export const dict = { "prompt.action.send": "전송", "prompt.action.stop": "중지", "prompt.toast.pasteUnsupported.title": "지원되지 않는 첨부 파일", + "prompt.toast.attachmentDuplicate.title": "이 파일은 이미 업로드되었습니다", "prompt.toast.pasteUnsupported.description": "이미지, PDF 또는 텍스트 파일만 첨부할 수 있습니다.", "prompt.toast.modelAgentRequired.title": "에이전트 및 모델 선택", "prompt.toast.modelAgentRequired.description": "프롬프트를 보내기 전에 에이전트와 모델을 선택하세요.", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index a4e8efb79d..959e4c68cb 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -288,6 +288,7 @@ export const dict = { "prompt.action.stop": "Stopp", "prompt.toast.pasteUnsupported.title": "Ikke støttet vedlegg", + "prompt.toast.attachmentDuplicate.title": "Denne filen er allerede lastet opp", "prompt.toast.pasteUnsupported.description": "Kun bilder, PDF-er eller tekstfiler kan legges ved her.", "prompt.toast.modelAgentRequired.title": "Velg en agent og modell", "prompt.toast.modelAgentRequired.description": "Velg en agent og modell før du sender en forespørsel.", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index f2e32ffeb8..7517f16b81 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -278,6 +278,7 @@ export const dict = { "prompt.action.send": "Wyślij", "prompt.action.stop": "Zatrzymaj", "prompt.toast.pasteUnsupported.title": "Nieobsługiwany załącznik", + "prompt.toast.attachmentDuplicate.title": "Ten plik został już przesłany", "prompt.toast.pasteUnsupported.description": "Można tutaj załączać tylko obrazy, pliki PDF lub pliki tekstowe.", "prompt.toast.modelAgentRequired.title": "Wybierz agenta i model", "prompt.toast.modelAgentRequired.description": "Wybierz agenta i model przed wysłaniem zapytania.", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index fa6fb04d04..17d6ba58a9 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -297,6 +297,7 @@ export const dict = { "prompt.action.stop": "Остановить", "prompt.toast.pasteUnsupported.title": "Неподдерживаемое вложение", + "prompt.toast.attachmentDuplicate.title": "Этот файл уже загружен", "prompt.toast.pasteUnsupported.description": "Здесь можно прикрепить только изображения, PDF или текстовые файлы.", "prompt.toast.modelAgentRequired.title": "Выберите агента и модель", "prompt.toast.modelAgentRequired.description": "Выберите агента и модель перед отправкой запроса.", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 0ae67ee34e..2c0b7d081c 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -297,6 +297,7 @@ export const dict = { "prompt.action.stop": "หยุด", "prompt.toast.pasteUnsupported.title": "ไฟล์แนบที่ไม่รองรับ", + "prompt.toast.attachmentDuplicate.title": "ไฟล์นี้ถูกอัปโหลดแล้ว", "prompt.toast.pasteUnsupported.description": "แนบได้เฉพาะรูปภาพ, PDF หรือไฟล์ข้อความเท่านั้น", "prompt.toast.modelAgentRequired.title": "เลือกเอเจนต์และโมเดล", "prompt.toast.modelAgentRequired.description": "เลือกเอเจนต์และโมเดลก่อนส่งพร้อมท์", diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts index d401519af9..7c123c69aa 100644 --- a/packages/app/src/i18n/tr.ts +++ b/packages/app/src/i18n/tr.ts @@ -302,6 +302,7 @@ export const dict = { "prompt.action.stop": "Durdur", "prompt.toast.pasteUnsupported.title": "Desteklenmeyen ek", + "prompt.toast.attachmentDuplicate.title": "Bu dosya zaten yüklendi", "prompt.toast.pasteUnsupported.description": "Buraya yalnızca resimler, PDF'ler veya metin dosyaları eklenebilir.", "prompt.toast.modelAgentRequired.title": "Bir ajan ve model seçin", "prompt.toast.modelAgentRequired.description": "Komut göndermeden önce bir ajan ve model seçin.", diff --git a/packages/app/src/i18n/uk.ts b/packages/app/src/i18n/uk.ts index c949f41de2..d8c9ae8e43 100644 --- a/packages/app/src/i18n/uk.ts +++ b/packages/app/src/i18n/uk.ts @@ -299,6 +299,7 @@ export const dict = { "prompt.action.stop": "Зупинити", "prompt.toast.pasteUnsupported.title": "Непідтримуване вкладення", + "prompt.toast.attachmentDuplicate.title": "Цей файл уже завантажено", "prompt.toast.pasteUnsupported.description": "Сюди можна прикріплювати лише зображення, PDF або текстові файли.", "prompt.toast.modelAgentRequired.title": "Виберіть агента та модель", "prompt.toast.modelAgentRequired.description": "Виберіть агента та модель перед надсиланням запиту.", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 82d73ac3a6..724bcc1175 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -316,6 +316,7 @@ export const dict = { "prompt.action.send": "发送", "prompt.action.stop": "停止", "prompt.toast.pasteUnsupported.title": "不支持的附件", + "prompt.toast.attachmentDuplicate.title": "此文件已上传", "prompt.toast.pasteUnsupported.description": "此处仅能附加图片、PDF 或文本文件。", "prompt.toast.modelAgentRequired.title": "请选择智能体和模型", "prompt.toast.modelAgentRequired.description": "发送提示前请先选择智能体和模型。", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index fbe80d95ee..6f66822165 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -297,6 +297,7 @@ export const dict = { "prompt.action.stop": "停止", "prompt.toast.pasteUnsupported.title": "不支援的附件", + "prompt.toast.attachmentDuplicate.title": "此檔案已上傳", "prompt.toast.pasteUnsupported.description": "此處僅能附加圖片、PDF 或文字檔案。", "prompt.toast.modelAgentRequired.title": "請選擇代理程式和模型", "prompt.toast.modelAgentRequired.description": "傳送提示前請先選擇代理程式和模型。", diff --git a/packages/app/test-browser/prompt-attachments.test.ts b/packages/app/test-browser/prompt-attachments.test.ts index ca7686598e..d604477e9e 100644 --- a/packages/app/test-browser/prompt-attachments.test.ts +++ b/packages/app/test-browser/prompt-attachments.test.ts @@ -1,7 +1,10 @@ import { describe, expect, test } from "bun:test" import { createRoot } from "solid-js" +import { createStore } from "solid-js/store" import { createPromptAttachmentsCore } from "@/components/prompt-input/attachments" import { createPromptState } from "@/context/prompt" +import { createPromptInputV2Attachments } from "../../session-ui/src/v2/components/prompt-input/attachments" +import type { PromptInputV2Prompt } from "../../session-ui/src/v2/components/prompt-input/types" describe("prompt attachment session ownership", () => { test("adds an asynchronously read image to the session where the read started", async () => { @@ -83,6 +86,89 @@ describe("prompt attachment session ownership", () => { dispose() }) }) + +}) + +test("rejects a duplicate native clipboard attachment in the V2 prompt store", async () => { + await createRoot(async (dispose) => { + const [state, setState] = createStore({ prompt: [] as PromptInputV2Prompt }) + const duplicate = Promise.withResolvers() + const files = [ + new File(["hello"], "clipboard-1.txt", { type: "text/plain" }), + new File(["hello"], "clipboard-2.txt", { type: "text/plain" }), + ] + const attachments = createPromptInputV2Attachments({ + capture: () => ({ + current: () => state.prompt, + cursor: () => 0, + set: (prompt) => setState("prompt", prompt), + }), + editor: () => document.createElement("div"), + focusEditor: () => undefined, + addPart: () => false, + setDraggingType: () => undefined, + directory: () => "/", + isDialogActive: () => false, + warn: () => undefined, + duplicate: duplicate.resolve, + onError: () => undefined, + readClipboardImage: async () => files.shift() ?? null, + }) + const event = { + clipboardData: { items: [], getData: () => "" }, + preventDefault: () => undefined, + stopPropagation: () => undefined, + } as unknown as ClipboardEvent + + await attachments.handlePaste(event) + await attachments.handlePaste(event) + await duplicate.promise + + expect(state.prompt).toHaveLength(1) + dispose() + }) +}) + +test("rejects desktop duplicates and keeps changed files in the V2 prompt store", async () => { + await createRoot(async (dispose) => { + const [state, setState] = createStore({ prompt: [] as PromptInputV2Prompt }) + const duplicates: string[] = [] + const attachments = createPromptInputV2Attachments({ + capture: () => ({ + current: () => state.prompt, + cursor: () => 0, + set: (prompt) => setState("prompt", prompt), + }), + editor: () => document.createElement("div"), + focusEditor: () => undefined, + addPart: () => false, + setDraggingType: () => undefined, + directory: () => "/", + isDialogActive: () => false, + warn: () => undefined, + duplicate: () => duplicates.push("duplicate"), + onError: () => undefined, + getPathForFile: (file) => (file.name === "browser.txt" ? "" : `/tmp/${file.name}`), + }) + const first = new File(["first"], "a.txt", { type: "text/plain" }) + const second = new File(["second"], "b.txt", { type: "text/plain" }) + + await attachments.addAttachments([first, second]) + await attachments.addAttachments([first, second]) + expect(state.prompt).toHaveLength(2) + expect(duplicates).toEqual(["duplicate", "duplicate"]) + + await attachments.addAttachments([new File(["edited"], "a.txt", { type: "text/plain" })]) + expect(state.prompt).toHaveLength(3) + + await attachments.addAttachments([ + new File(["same"], "browser.txt", { type: "text/plain" }), + new File(["same"], "browser.txt", { type: "text/plain" }), + ]) + expect(state.prompt).toHaveLength(4) + expect(duplicates).toHaveLength(3) + dispose() + }) }) function images(prompt: ReturnType) { diff --git a/packages/session-ui/src/v2/components/prompt-input/attachments.ts b/packages/session-ui/src/v2/components/prompt-input/attachments.ts index 8c7b12e9ae..cfe3c43bd8 100644 --- a/packages/session-ui/src/v2/components/prompt-input/attachments.ts +++ b/packages/session-ui/src/v2/components/prompt-input/attachments.ts @@ -74,6 +74,7 @@ export type PromptInputV2AttachmentConfig = { directory: () => string isDialogActive: () => boolean warn: () => void + duplicate: () => void onError: (error: unknown) => void readClipboardImage?: () => Promise getPathForFile?: (file: File) => string @@ -94,7 +95,7 @@ export function createPromptInputV2Attachments( if (!editor) return return { prompt, cursor: prompt.cursor() ?? cursorPosition(editor) } } - const add = async (file: File, toast = true, target = capture()) => { + const add = async (file: File, toast = true, target = capture(), clipboard = false) => { if (!target) return false const mime = await attachmentMime(file) if (!mime) { @@ -103,11 +104,26 @@ export function createPromptInputV2Attachments( } const url = await dataUrl(file, mime) if (!url) return false + const sourcePath = input.getPathForFile?.(file) || undefined + // Native clipboard images arrive with a fresh timestamped filename on every paste, so identical + // clipboard content is matched on bytes alone. + const duplicate = target.prompt + .current() + .some( + (part) => + part.type === "image" && + part.dataUrl === url && + (sourcePath ? part.sourcePath === sourcePath : !part.sourcePath && (clipboard || part.filename === file.name)), + ) + if (duplicate) { + input.duplicate() + return true + } const attachment: PromptInputV2Attachment = { type: "image", id: globalThis.crypto?.randomUUID?.() ?? Math.random().toString(16).slice(2), filename: file.name, - sourcePath: input.getPathForFile?.(file) || undefined, + sourcePath, mime, dataUrl: url, } @@ -141,7 +157,7 @@ export function createPromptInputV2Attachments( const plainText = clipboardData.getData("text/plain") ?? "" if (input.readClipboardImage && !plainText) { const file = await input.readClipboardImage() - if (file && (await add(file, true, target))) return + if (file && (await add(file, true, target, true))) return } if (!plainText) return const text = plainText.includes("\r") ? plainText.replace(/\r\n?/g, "\n") : plainText From 005bf088c9371ab886affed91b021fae33388503 Mon Sep 17 00:00:00 2001 From: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:33:23 +0530 Subject: [PATCH 084/129] feat(app): fix file tree button bg (#38629) --- packages/session-ui/src/v2/components/session-review-v2.css | 2 +- packages/session-ui/src/v2/components/session-review-v2.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/session-ui/src/v2/components/session-review-v2.css b/packages/session-ui/src/v2/components/session-review-v2.css index 751c258a52..e28a6787d4 100644 --- a/packages/session-ui/src/v2/components/session-review-v2.css +++ b/packages/session-ui/src/v2/components/session-review-v2.css @@ -244,7 +244,7 @@ } [data-component="icon-button-v2"][data-variant="ghost"].session-review-v2-sidebar-toggle[aria-expanded="true"] { - background-color: var(--v2-overlay-simple-overlay-pressed); + background-color: var(--v2-background-bg-layer-02); } [data-component="session-review-v2"] [data-slot="session-review-v2-file-header"] { diff --git a/packages/session-ui/src/v2/components/session-review-v2.tsx b/packages/session-ui/src/v2/components/session-review-v2.tsx index bfd6778c54..d2f3b6b8a6 100644 --- a/packages/session-ui/src/v2/components/session-review-v2.tsx +++ b/packages/session-ui/src/v2/components/session-review-v2.tsx @@ -333,6 +333,7 @@ export function SessionReviewV2SidebarToggle(props: { opened: boolean; disabled? class="session-review-v2-sidebar-toggle" aria-label={i18n.t("ui.sessionReviewV2.toggleSidebar")} aria-expanded={props.opened} + data-expanded={props.opened ? "" : undefined} disabled={props.disabled} onClick={props.onToggle} icon={} From a277557a5fa767c59a301aacf7f7dfa07c8a6060 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 30 Jul 2026 09:05:12 +0000 Subject: [PATCH 085/129] chore: generate --- packages/app/test-browser/prompt-attachments.test.ts | 1 - .../session-ui/src/v2/components/prompt-input/attachments.ts | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/app/test-browser/prompt-attachments.test.ts b/packages/app/test-browser/prompt-attachments.test.ts index d604477e9e..49dad279cd 100644 --- a/packages/app/test-browser/prompt-attachments.test.ts +++ b/packages/app/test-browser/prompt-attachments.test.ts @@ -86,7 +86,6 @@ describe("prompt attachment session ownership", () => { dispose() }) }) - }) test("rejects a duplicate native clipboard attachment in the V2 prompt store", async () => { diff --git a/packages/session-ui/src/v2/components/prompt-input/attachments.ts b/packages/session-ui/src/v2/components/prompt-input/attachments.ts index cfe3c43bd8..afa97a7f3e 100644 --- a/packages/session-ui/src/v2/components/prompt-input/attachments.ts +++ b/packages/session-ui/src/v2/components/prompt-input/attachments.ts @@ -113,7 +113,9 @@ export function createPromptInputV2Attachments( (part) => part.type === "image" && part.dataUrl === url && - (sourcePath ? part.sourcePath === sourcePath : !part.sourcePath && (clipboard || part.filename === file.name)), + (sourcePath + ? part.sourcePath === sourcePath + : !part.sourcePath && (clipboard || part.filename === file.name)), ) if (duplicate) { input.duplicate() From 3dbce3dade7373ff532f3ed82af9b389ff073f44 Mon Sep 17 00:00:00 2001 From: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:37:06 +0530 Subject: [PATCH 086/129] feat(app): adds fades to file tree (#38549) --- .../src/v2/components/session-review-v2.css | 67 ++++++++++++++++++- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/packages/session-ui/src/v2/components/session-review-v2.css b/packages/session-ui/src/v2/components/session-review-v2.css index e28a6787d4..704e8fd7d7 100644 --- a/packages/session-ui/src/v2/components/session-review-v2.css +++ b/packages/session-ui/src/v2/components/session-review-v2.css @@ -63,7 +63,7 @@ align-items: center; justify-content: space-between; gap: 8px; - padding: 12px 16px 8px 8px; + padding: 12px 16px 12px 8px; flex-shrink: 0; } @@ -91,7 +91,7 @@ } [data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-filter"] { - padding: 0 8px 8px; + padding: 0 8px 4px; flex-shrink: 0; min-width: 0; box-sizing: border-box; @@ -109,10 +109,71 @@ [data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"] { flex: 1; min-height: 0; + timeline-scope: --session-review-v2-sidebar-tree-scroll; +} + +@keyframes session-review-v2-sidebar-tree-fade-top { + from { + visibility: hidden; + } + to { + visibility: visible; + } +} + +@keyframes session-review-v2-sidebar-tree-fade-bottom { + from { + visibility: visible; + } + to { + visibility: hidden; + } +} + +[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::before, +[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::after { + content: ""; + position: absolute; + inset-inline: 0; + z-index: 10; + height: 16px; + pointer-events: none; + visibility: hidden; +} + +[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::before { + top: 0; + background: linear-gradient(to bottom, var(--v2-background-bg-base), transparent); +} + +[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::after { + bottom: 0; + background: linear-gradient(to top, var(--v2-background-bg-base), transparent); +} + +@supports (animation-timeline: --session-review-v2-sidebar-tree-scroll) and + (timeline-scope: --session-review-v2-sidebar-tree-scroll) { + [data-component="session-review-v2-sidebar-root"] + [data-slot="session-review-v2-sidebar-tree"] + .scroll-view__viewport { + scroll-timeline: --session-review-v2-sidebar-tree-scroll y; + } + + [data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::before { + animation: session-review-v2-sidebar-tree-fade-top linear both; + animation-timeline: --session-review-v2-sidebar-tree-scroll; + animation-range: 0 0.1px; + } + + [data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::after { + animation: session-review-v2-sidebar-tree-fade-bottom linear both; + animation-timeline: --session-review-v2-sidebar-tree-scroll; + animation-range: calc(100% - 1.1px) calc(100% - 1px); + } } [data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"] .scroll-view__viewport { - padding: 0 8px 12px; + padding: 4px 8px 12px; } [data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"] .scroll-view__thumb { From c5cf416633e4b12daf21b4ea230d52f2dae03f32 Mon Sep 17 00:00:00 2001 From: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:37:35 +0530 Subject: [PATCH 087/129] feat(app): fix composer attachment fades (#38547) --- .../components/prompt-input/attachments.css | 44 +++++++++++++++++++ .../src/v2/components/prompt-input/index.tsx | 15 +++++-- 2 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 packages/session-ui/src/v2/components/prompt-input/attachments.css diff --git a/packages/session-ui/src/v2/components/prompt-input/attachments.css b/packages/session-ui/src/v2/components/prompt-input/attachments.css new file mode 100644 index 0000000000..3b8173d105 --- /dev/null +++ b/packages/session-ui/src/v2/components/prompt-input/attachments.css @@ -0,0 +1,44 @@ +@keyframes prompt-input-v2-attachments-fade-left { + from { + visibility: hidden; + } + to { + visibility: visible; + } +} + +@keyframes prompt-input-v2-attachments-fade-right { + from { + visibility: visible; + } + to { + visibility: hidden; + } +} + +[data-component="prompt-input-v2-attachments"] { + timeline-scope: --prompt-input-v2-attachments-scroll; + + [data-slot^="prompt-attachments-fade-"] { + visibility: hidden; + } +} + +@supports (animation-timeline: --prompt-input-v2-attachments-scroll) and + (timeline-scope: --prompt-input-v2-attachments-scroll) { + [data-component="prompt-input-v2-attachments"] [data-slot="prompt-attachments-scroll"] { + scroll-timeline: --prompt-input-v2-attachments-scroll x; + } + + [data-component="prompt-input-v2-attachments"] [data-slot="prompt-attachments-fade-left"] { + animation: prompt-input-v2-attachments-fade-left linear both; + animation-timeline: --prompt-input-v2-attachments-scroll; + animation-range: 0 0.1px; + } + + [data-component="prompt-input-v2-attachments"] [data-slot="prompt-attachments-fade-right"] { + animation: prompt-input-v2-attachments-fade-right linear both; + animation-timeline: --prompt-input-v2-attachments-scroll; + animation-range: calc(100% - 1.1px) calc(100% - 1px); + } +} diff --git a/packages/session-ui/src/v2/components/prompt-input/index.tsx b/packages/session-ui/src/v2/components/prompt-input/index.tsx index c09b5a1b5f..0989e2ef65 100644 --- a/packages/session-ui/src/v2/components/prompt-input/index.tsx +++ b/packages/session-ui/src/v2/components/prompt-input/index.tsx @@ -21,6 +21,7 @@ import type { PromptInputV2Suggestion, } from "./types" import type { PromptInputV2Interaction, PromptInputV2SelectControl } from "./interaction" +import "./attachments.css" export type { PromptInputV2Attachment, @@ -107,7 +108,7 @@ export function PromptInputV2(props: PromptInputV2Props) {
0 || (props.comments?.length ?? 0) > 0}> -
+
-
-
+
+
) From c08350b83e3d976c0f93af68d22f5b1518194f30 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 30 Jul 2026 09:11:45 +0000 Subject: [PATCH 088/129] 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 59ba15e5b2..a7b441535c 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-Z/YgK5sf0L9MG7hMIb+wj3ias6iNo8L4oFHYNtRHreM=", - "aarch64-linux": "sha256-MoYZmeh6RcDUN7a132wexFNwkHz+Q9Ijs+qiacuKJ90=", - "aarch64-darwin": "sha256-OHo3x7UcJzrPMAF9Ij/9Rhamd+dfpb5QuxP89cy7Rw4=", - "x86_64-darwin": "sha256-ElzZVqkQPYTAd0nvLCB5uel9xtOqMK07QFW8MDr174E=" + "x86_64-linux": "sha256-fK6zAHJC3ut/KUdfqLPxVMH7Z1yv65YZ7qcHF45kPas=", + "aarch64-linux": "sha256-CSWZY/vVUCKzlwfisQQHqHjawUwL0BRc8Y9o2y/QZYA=", + "aarch64-darwin": "sha256-IyFm5NbnU63BaOO/F4/v1exz3VbvkY96yjg9iun+O9Q=", + "x86_64-darwin": "sha256-XnXFoUxqXmEx7AKx5YhaJuLY2NYMtI0ICafaenU5L6k=" } } From ac5380614f310586949e0947b1be244268ebdb7c Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:29:40 +0200 Subject: [PATCH 089/129] fix(app): repair malformed persisted tabs (#39645) --- packages/app/src/context/tab-migration.ts | 23 +++++++++++++++++++++++ packages/app/src/context/tabs.test.ts | 23 +++++++++++++++++++++++ packages/app/src/context/tabs.tsx | 9 ++------- 3 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 packages/app/src/context/tab-migration.ts diff --git a/packages/app/src/context/tab-migration.ts b/packages/app/src/context/tab-migration.ts new file mode 100644 index 0000000000..04c1cb6804 --- /dev/null +++ b/packages/app/src/context/tab-migration.ts @@ -0,0 +1,23 @@ +import type { ServerConnection } from "./server" +import type { Tab } from "./tabs" + +export function migrateTabs(value: unknown, fallback: ServerConnection.Key): Tab[] { + if (!Array.isArray(value)) return [] + return value.flatMap((tab) => { + if (!tab || typeof tab !== "object") return [] + if ("server" in tab && typeof tab.server !== "string") return [] + const server = ("server" in tab ? tab.server : fallback) as ServerConnection.Key + if (tab.type === "session" && typeof tab.sessionId === "string") { + return [{ type: tab.type, server, sessionId: tab.sessionId }] + } + if ( + tab.type === "draft" && + typeof tab.draftID === "string" && + typeof tab.directory === "string" && + (tab.worktree === undefined || typeof tab.worktree === "string") + ) { + return [{ type: tab.type, server, draftID: tab.draftID, directory: tab.directory, worktree: tab.worktree }] + } + return [] + }) +} diff --git a/packages/app/src/context/tabs.test.ts b/packages/app/src/context/tabs.test.ts index b56ea2dfcf..17371efd80 100644 --- a/packages/app/src/context/tabs.test.ts +++ b/packages/app/src/context/tabs.test.ts @@ -3,6 +3,7 @@ import { createRoot, getOwner, onCleanup } from "solid-js" import { createTabMemory } from "./tab-memory" import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs" import type { SessionTab, Tab } from "./tabs" +import { migrateTabs } from "./tab-migration" import type { ServerConnection } from "./server" const server = "local\nhttp://localhost:4096" as ServerConnection.Key @@ -11,6 +12,28 @@ function sessionTab(sessionId: string): SessionTab { return { type: "session", server, sessionId } } +describe("tab migration", () => { + test("drops null and malformed persisted tabs", () => { + expect( + migrateTabs( + [null, sessionTab("a"), { type: "session", server }, { type: "unknown", server }, "invalid"], + server, + ), + ).toEqual([sessionTab("a")]) + }) + + test("adds the fallback server to valid legacy tabs", () => { + expect(migrateTabs([{ type: "session", sessionId: "a", dirBase64: "legacy" }], server)).toEqual([ + sessionTab("a"), + ]) + }) + + test("replaces invalid top-level persisted data", () => { + expect(migrateTabs(null, server)).toEqual([]) + expect(migrateTabs({}, server)).toEqual([]) + }) +}) + describe("tab memory", () => { test("keeps state until its tab is removed", () => { createRoot((dispose) => { diff --git a/packages/app/src/context/tabs.tsx b/packages/app/src/context/tabs.tsx index 75191d54cf..e4d50ac161 100644 --- a/packages/app/src/context/tabs.tsx +++ b/packages/app/src/context/tabs.tsx @@ -12,6 +12,7 @@ import { sessionHref } from "@/utils/session-route" import { createTabMemory } from "./tab-memory" import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs" import { createDraftPromptSession, type PromptModel } from "./prompt-state" +import { migrateTabs } from "./tab-migration" export type SessionTab = { type: "session" @@ -59,13 +60,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ const [store, setStore, _, ready] = persisted( { ...Persist.window("tabs"), - migrate: (value: unknown) => { - if (!Array.isArray(value)) return value - return value.map((tab) => { - if (!tab || typeof tab !== "object" || "server" in tab) return tab - return { ...tab, server: fallback } - }) - }, + migrate: (value: unknown) => migrateTabs(value, fallback), }, createStore([]), ) From ff0382e97145cb6585b575dcc1269fa1512e853b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 30 Jul 2026 09:31:27 +0000 Subject: [PATCH 090/129] chore: generate --- packages/app/src/context/tabs.test.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/app/src/context/tabs.test.ts b/packages/app/src/context/tabs.test.ts index 17371efd80..abd62a4dae 100644 --- a/packages/app/src/context/tabs.test.ts +++ b/packages/app/src/context/tabs.test.ts @@ -15,17 +15,12 @@ function sessionTab(sessionId: string): SessionTab { describe("tab migration", () => { test("drops null and malformed persisted tabs", () => { expect( - migrateTabs( - [null, sessionTab("a"), { type: "session", server }, { type: "unknown", server }, "invalid"], - server, - ), + migrateTabs([null, sessionTab("a"), { type: "session", server }, { type: "unknown", server }, "invalid"], server), ).toEqual([sessionTab("a")]) }) test("adds the fallback server to valid legacy tabs", () => { - expect(migrateTabs([{ type: "session", sessionId: "a", dirBase64: "legacy" }], server)).toEqual([ - sessionTab("a"), - ]) + expect(migrateTabs([{ type: "session", sessionId: "a", dirBase64: "legacy" }], server)).toEqual([sessionTab("a")]) }) test("replaces invalid top-level persisted data", () => { From a4f25a94b4e470ec10cd97a0c8e2f2c23e215b38 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:12:08 -0400 Subject: [PATCH 091/129] fix: use OpenCode model catalog URL (#39672) Co-authored-by: Dax Raad --- packages/cli/script/generate.ts | 2 +- packages/core/src/models-dev.ts | 4 ++-- packages/llm/script/recording-cost-report.ts | 2 +- packages/stats/app/src/routes/model-catalog.ts | 6 +++--- packages/ui/vite.config.ts | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/cli/script/generate.ts b/packages/cli/script/generate.ts index d98565e298..e162f2ea7e 100755 --- a/packages/cli/script/generate.ts +++ b/packages/cli/script/generate.ts @@ -1,4 +1,4 @@ -const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev" +const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.opencode.ai" export const modelsData = process.env.MODELS_DEV_API_JSON ? await Bun.file(process.env.MODELS_DEV_API_JSON).text() diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index 602133d904..7e3118722f 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -151,10 +151,10 @@ const layer = Layer.effect( ), ) - const source = Flag.OPENCODE_MODELS_URL || "https://models.dev" + const source = Flag.OPENCODE_MODELS_URL || "https://models.opencode.ai" const filepath = path.join( Global.Path.cache, - source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`, + source === "https://models.opencode.ai" ? "models.json" : `models-${Hash.fast(source)}.json`, ) const ttl = Duration.minutes(5) const lockKey = `models-dev:${filepath}` diff --git a/packages/llm/script/recording-cost-report.ts b/packages/llm/script/recording-cost-report.ts index 5b08e72d5c..1f42dc5932 100644 --- a/packages/llm/script/recording-cost-report.ts +++ b/packages/llm/script/recording-cost-report.ts @@ -2,7 +2,7 @@ import * as fs from "node:fs/promises" import * as path from "node:path" const RECORDINGS_DIR = path.resolve(import.meta.dir, "..", "test", "fixtures", "recordings") -const MODELS_DEV_URL = "https://models.dev/api.json" +const MODELS_DEV_URL = "https://models.opencode.ai/api.json" type JsonRecord = Record diff --git a/packages/stats/app/src/routes/model-catalog.ts b/packages/stats/app/src/routes/model-catalog.ts index efd6f6b2e0..47fa1cf347 100644 --- a/packages/stats/app/src/routes/model-catalog.ts +++ b/packages/stats/app/src/routes/model-catalog.ts @@ -1,8 +1,8 @@ import { query } from "@solidjs/router" -export const modelCatalogSourceUrl = "https://models.dev/catalog.json" -export const modelCatalogPricingUrl = "https://models.dev/api.json" -export const modelCatalogLabSourceUrl = "https://models.dev/labs" +export const modelCatalogSourceUrl = "https://models.opencode.ai/catalog.json" +export const modelCatalogPricingUrl = "https://models.opencode.ai/api.json" +export const modelCatalogLabSourceUrl = "https://models.opencode.ai/labs" export type ModelCatalogCost = { input: number diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts index 1e38adede2..1d5af6934c 100644 --- a/packages/ui/vite.config.ts +++ b/packages/ui/vite.config.ts @@ -45,7 +45,7 @@ function providerIconsPlugin() { } async function fetchProviderIcons() { - const url = process.env.OPENCODE_MODELS_URL || "https://models.dev" + const url = process.env.OPENCODE_MODELS_URL || "https://models.opencode.ai" const providers = await fetch(`${url}/api.json`) .then((res) => res.json()) .then((json) => Object.keys(json)) From 8c38d260eb6555d2824230be100fb2a7eadd7513 Mon Sep 17 00:00:00 2001 From: opencode Date: Thu, 30 Jul 2026 14:43:50 +0000 Subject: [PATCH 092/129] sync release versions for v1.18.10 --- bun.lock | 56 ++++++++++----------- packages/app/package.json | 2 +- packages/cli/package.json | 2 +- packages/codemode/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/console/support/package.json | 2 +- packages/core/package.json | 2 +- packages/desktop/package.json | 2 +- packages/effect-drizzle-sqlite/package.json | 2 +- packages/effect-sqlite-node/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/function/package.json | 2 +- packages/http-recorder/package.json | 2 +- packages/llm/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/server/package.json | 2 +- packages/session-ui/package.json | 2 +- packages/slack/package.json | 2 +- packages/stats/app/package.json | 2 +- packages/stats/core/package.json | 2 +- packages/stats/server/package.json | 2 +- packages/tui/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 30 files changed, 57 insertions(+), 57 deletions(-) diff --git a/bun.lock b/bun.lock index 9f6ef7b628..160958f8da 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", @@ -96,7 +96,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.18.9", + "version": "1.18.10", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -144,7 +144,7 @@ }, "packages/codemode": { "name": "@opencode-ai/codemode", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "acorn": "8.15.0", "effect": "catalog:", @@ -158,7 +158,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -194,7 +194,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -221,7 +221,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -243,7 +243,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -267,7 +267,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -287,7 +287,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.18.9", + "version": "1.18.10", "bin": { "opencode": "./bin/opencode", }, @@ -381,7 +381,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@zip.js/zip.js": "2.7.62", "effect": "catalog:", @@ -435,7 +435,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -449,7 +449,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "effect": "catalog:", }, @@ -461,7 +461,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -493,7 +493,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -509,7 +509,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83", @@ -540,7 +540,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", @@ -559,7 +559,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.18.9", + "version": "1.18.10", "bin": { "opencode": "./bin/opencode", }, @@ -690,7 +690,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "workspace:*", @@ -766,7 +766,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "cross-spawn": "catalog:", }, @@ -781,7 +781,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", @@ -796,7 +796,7 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", @@ -841,7 +841,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -854,7 +854,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@ibm/plex": "6.4.1", "@kobalte/core": "catalog:", @@ -888,7 +888,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -907,7 +907,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -949,7 +949,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -976,7 +976,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1028,7 +1028,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.18.9", + "version": "1.18.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 08eb9a059f..e6e95e35ff 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.18.9", + "version": "1.18.10", "description": "", "type": "module", "exports": { diff --git a/packages/cli/package.json b/packages/cli/package.json index d64acf4840..46a372229b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/cli", - "version": "1.18.9", + "version": "1.18.10", "type": "module", "license": "MIT", "bin": { diff --git a/packages/codemode/package.json b/packages/codemode/package.json index 83cfeaac0c..e0c89ec26e 100644 --- a/packages/codemode/package.json +++ b/packages/codemode/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/codemode", - "version": "1.18.9", + "version": "1.18.10", "description": "Effect-native confined code execution over schema-described tools", "private": true, "type": "module", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index b8fd9a04c6..f27b89305f 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.18.9", + "version": "1.18.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index f922ab9ef2..63ba0a50f2 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.18.9", + "version": "1.18.10", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index aadfa42be1..52581a8c93 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.18.9", + "version": "1.18.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 1a551ed0a5..f54268c9c8 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.18.9", + "version": "1.18.10", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/console/support/package.json b/packages/console/support/package.json index 6d90a394cb..6b6a65b6e6 100644 --- a/packages/console/support/package.json +++ b/packages/console/support/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-support", - "version": "1.18.9", + "version": "1.18.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index cc1f5d6c61..a8a5041e73 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.9", + "version": "1.18.10", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 1a5baf91b0..d00b2bda45 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.18.9", + "version": "1.18.10", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index 6ae8d912a1..9577e7a463 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.9", + "version": "1.18.10", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/effect-sqlite-node/package.json b/packages/effect-sqlite-node/package.json index d26bea7f85..7c084da87b 100644 --- a/packages/effect-sqlite-node/package.json +++ b/packages/effect-sqlite-node/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.9", + "version": "1.18.10", "name": "@opencode-ai/effect-sqlite-node", "type": "module", "license": "MIT", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index d298dd1dee..117d2220ad 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.18.9", + "version": "1.18.10", "private": true, "type": "module", "license": "MIT", diff --git a/packages/function/package.json b/packages/function/package.json index 6acd31710e..f8e1ee3d86 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.18.9", + "version": "1.18.10", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index cca67e0c30..56224a41c7 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.9", + "version": "1.18.10", "name": "@opencode-ai/http-recorder", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "type": "module", diff --git a/packages/llm/package.json b/packages/llm/package.json index 69e1ea92eb..d575867f1c 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.9", + "version": "1.18.10", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 105f307319..84f03d88ab 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.9", + "version": "1.18.10", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 2607c317d6..33b6d2b0dc 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.18.9", + "version": "1.18.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index f92dca0c4d..68313859b6 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.18.9", + "version": "1.18.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 89d7360a79..fcfa76143e 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/server", - "version": "1.18.9", + "version": "1.18.10", "private": true, "type": "module", "license": "MIT", diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index eb67c17d19..f34feebf25 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/session-ui", - "version": "1.18.9", + "version": "1.18.10", "private": true, "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index 7ec9f938b7..20f1ce0598 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.18.9", + "version": "1.18.10", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json index 8e69fd7193..3a19ff5aad 100644 --- a/packages/stats/app/package.json +++ b/packages/stats/app/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-app", - "version": "1.18.9", + "version": "1.18.10", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/core/package.json b/packages/stats/core/package.json index 45e6acf976..c139702674 100644 --- a/packages/stats/core/package.json +++ b/packages/stats/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-core", - "version": "1.18.9", + "version": "1.18.10", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/server/package.json b/packages/stats/server/package.json index 5d34525aa1..efa68e21dc 100644 --- a/packages/stats/server/package.json +++ b/packages/stats/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-server", - "version": "1.18.9", + "version": "1.18.10", "private": true, "type": "module", "license": "MIT", diff --git a/packages/tui/package.json b/packages/tui/package.json index 9900a37849..a96b6a7088 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/tui", - "version": "1.18.9", + "version": "1.18.10", "private": true, "type": "module", "license": "MIT", diff --git a/packages/ui/package.json b/packages/ui/package.json index f994110b9f..36cd5ad027 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.18.9", + "version": "1.18.10", "type": "module", "license": "MIT", "repository": { diff --git a/packages/web/package.json b/packages/web/package.json index bdc41cdd67..260ca51941 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.18.9", + "version": "1.18.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 33c79c001d..15943599b3 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.18.9", + "version": "1.18.10", "publisher": "sst-dev", "repository": { "type": "git", From a1ab489e6175cabbb3b46a15c2efac78d49d5083 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:10:34 -0500 Subject: [PATCH 093/129] fix(provider): widen interleaved reasoning fields (#39556) Co-authored-by: Aiden Cline --- packages/core/src/models-dev.ts | 10 ++++-- packages/core/src/v1/config/provider.ts | 10 ++++-- packages/opencode/src/provider/provider.ts | 11 +++++-- .../opencode/test/provider/provider.test.ts | 6 ++++ packages/sdk/js/src/v2/gen/types.gen.ts | 10 ++++-- packages/sdk/openapi.json | 32 +++++++++++++++---- 6 files changed, 63 insertions(+), 16 deletions(-) diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index 7e3118722f..e577935f06 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -15,6 +15,11 @@ import { httpClient } from "./effect/app-node-platform" export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"]) export type CatalogModelStatus = typeof CatalogModelStatus.Type +const InterleavedField = Schema.Union([ + Schema.Literals(["reasoning", "reasoning_content", "reasoning_text"]), + Schema.String, +]) + const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}` const CostTier = Schema.Struct({ @@ -71,9 +76,10 @@ export const Model = Schema.Struct({ reasoning_options: Schema.optional(Schema.Array(ReasoningOption)), interleaved: Schema.optional( Schema.Union([ - Schema.Literal(true), + Schema.Boolean, + InterleavedField, Schema.Struct({ - field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]), + field: InterleavedField, }), ]), ), diff --git a/packages/core/src/v1/config/provider.ts b/packages/core/src/v1/config/provider.ts index d54a3f08f9..f860a2b4ac 100644 --- a/packages/core/src/v1/config/provider.ts +++ b/packages/core/src/v1/config/provider.ts @@ -5,6 +5,11 @@ import { PositiveInt } from "../../schema" export const ModelStatus = Schema.Literals(["alpha", "beta", "deprecated", "active"]) +const InterleavedField = Schema.Union([ + Schema.Literals(["reasoning", "reasoning_content", "reasoning_text"]), + Schema.String, +]) + export const Model = Schema.Struct({ id: Schema.optional(Schema.String), name: Schema.optional(Schema.String), @@ -16,9 +21,10 @@ export const Model = Schema.Struct({ tool_call: Schema.optional(Schema.Boolean), interleaved: Schema.optional( Schema.Union([ - Schema.Literal(true), + Schema.Boolean, + InterleavedField, Schema.Struct({ - field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]), + field: InterleavedField, }), ]), ), diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index a0ab5eb347..85b7fd3978 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -976,10 +976,15 @@ const ProviderModalities = Schema.Struct({ pdf: Schema.Boolean, }) +const ProviderInterleavedField = Schema.Union([ + Schema.Literals(["reasoning", "reasoning_content", "reasoning_text"]), + Schema.String, +]) + const ProviderInterleaved = Schema.Union([ Schema.Boolean, Schema.Struct({ - field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]), + field: ProviderInterleavedField, }), ]) @@ -1243,7 +1248,7 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model video: model.modalities?.output?.includes("video") ?? false, pdf: model.modalities?.output?.includes("pdf") ?? false, }, - interleaved: model.interleaved ?? false, + interleaved: typeof model.interleaved === "string" ? { field: model.interleaved } : (model.interleaved ?? false), }, release_date: model.release_date ?? "", variants: {}, @@ -1475,7 +1480,7 @@ const layer = Layer.effect( pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false, }, interleaved: - model.interleaved ?? + (typeof model.interleaved === "string" ? { field: model.interleaved } : model.interleaved) ?? existingModel?.capabilities.interleaved ?? (!existingModel && apiNpm === "@ai-sdk/openai-compatible" && apiID.includes("deepseek") ? { field: "reasoning_content" } diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 058bad23a1..44e82c7e1f 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -252,6 +252,8 @@ it.instance( const provider = providers[ProviderV2.ID.make("custom-provider")] expect(provider.models["deepseek-r1"].capabilities.interleaved).toEqual({ field: "reasoning_content" }) expect(provider.models["deepseek-details"].capabilities.interleaved).toEqual({ field: "reasoning_details" }) + expect(provider.models["deepseek-text"].capabilities.interleaved).toEqual({ field: "reasoning_text" }) + expect(provider.models["custom-reasoning"].capabilities.interleaved).toEqual({ field: "vendor_reasoning" }) expect(provider.models["custom-model"].capabilities.interleaved).toBe(false) expect( providers[ProviderV2.ID.make("custom-anthropic-provider")].models["deepseek-r1"].capabilities.interleaved, @@ -267,6 +269,8 @@ it.instance( models: { "deepseek-r1": { name: "DeepSeek R1" }, "deepseek-details": { name: "DeepSeek Details", interleaved: { field: "reasoning_details" } }, + "deepseek-text": { name: "DeepSeek Text", interleaved: "reasoning_text" }, + "custom-reasoning": { name: "Custom Reasoning", interleaved: { field: "vendor_reasoning" } }, "custom-model": { name: "Custom Model" }, }, options: { apiKey: "custom-key" }, @@ -1462,6 +1466,7 @@ test("models.dev normalization fills required response fields", () => { id: "gpt-5.4", name: "GPT-5.4", family: "gpt", + interleaved: "reasoning_text", cost: { input: 2.5, output: 15 }, limit: { context: 1_050_000, input: 922_000, output: 128_000 }, }, @@ -1474,6 +1479,7 @@ test("models.dev normalization fills required response fields", () => { expect(model.capabilities.reasoning).toBe(false) expect(model.capabilities.attachment).toBe(false) expect(model.capabilities.toolcall).toBe(true) + expect(model.capabilities.interleaved).toEqual({ field: "reasoning_text" }) expect(model.release_date).toBe("") }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 42d224780d..90c91e9158 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1768,9 +1768,13 @@ export type ProviderConfig = { temperature?: boolean tool_call?: boolean interleaved?: - | true + | boolean + | "reasoning" + | "reasoning_content" + | "reasoning_text" + | string | { - field: "reasoning" | "reasoning_content" | "reasoning_details" + field: "reasoning" | "reasoning_content" | "reasoning_text" | string } cost?: { input: number @@ -2057,7 +2061,7 @@ export type Model = { interleaved: | boolean | { - field: "reasoning" | "reasoning_content" | "reasoning_details" + field: "reasoning" | "reasoning_content" | "reasoning_text" | string } } cost: { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index b300754bc8..afe14bb604 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -20820,15 +20820,28 @@ "interleaved": { "anyOf": [ { - "type": "boolean", - "enum": [true] + "type": "boolean" + }, + { + "type": "string", + "enum": ["reasoning", "reasoning_content", "reasoning_text"] + }, + { + "type": "string" }, { "type": "object", "properties": { "field": { - "type": "string", - "enum": ["reasoning", "reasoning_content", "reasoning_details"] + "anyOf": [ + { + "type": "string", + "enum": ["reasoning", "reasoning_content", "reasoning_text"] + }, + { + "type": "string" + } + ] } }, "required": ["field"], @@ -21641,8 +21654,15 @@ "type": "object", "properties": { "field": { - "type": "string", - "enum": ["reasoning", "reasoning_content", "reasoning_details"] + "anyOf": [ + { + "type": "string", + "enum": ["reasoning", "reasoning_content", "reasoning_text"] + }, + { + "type": "string" + } + ] } }, "required": ["field"], From c1ee3c6e344c1822814a1a18e99bc14cd1ea36f3 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:26:48 -0500 Subject: [PATCH 094/129] fix(opencode): stop MCP SSE error reconnect loops (#39697) --- packages/opencode/test/mcp/transport.test.ts | 38 +++++++++++++++++++ .../@modelcontextprotocol%2Fsdk@1.29.0.patch | 20 +++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 packages/opencode/test/mcp/transport.test.ts diff --git a/packages/opencode/test/mcp/transport.test.ts b/packages/opencode/test/mcp/transport.test.ts new file mode 100644 index 0000000000..f7a74d4d72 --- /dev/null +++ b/packages/opencode/test/mcp/transport.test.ts @@ -0,0 +1,38 @@ +import { expect, test } from "bun:test" +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" + +test("does not reconnect an SSE stream after a JSON-RPC error response", async () => { + let requests = 0 + const transport = new StreamableHTTPClientTransport(new URL("http://mcp.invalid"), { + fetch: async () => { + requests += 1 + return new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("id: prime\nretry: 1\ndata:\n\n")) + controller.enqueue( + new TextEncoder().encode( + 'id: error\ndata: {"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found"},"id":1}\n\n', + ), + ) + controller.close() + }, + }), + { status: 200, headers: { "content-type": "text/event-stream" } }, + ) + }, + reconnectionOptions: { + initialReconnectionDelay: 1, + maxReconnectionDelay: 1, + reconnectionDelayGrowFactor: 1, + maxRetries: 2, + }, + }) + + await transport.start() + await transport.send({ jsonrpc: "2.0", method: "resources/list", id: 1 }) + await Bun.sleep(25) + await transport.close() + + expect(requests).toBe(1) +}) diff --git a/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch b/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch index 13b8000a01..3b70bea558 100644 --- a/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch +++ b/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch @@ -141,6 +141,15 @@ diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp. index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eedff5443c9 100644 --- a/dist/cjs/client/streamableHttp.js +++ b/dist/cjs/client/streamableHttp.js +@@ -204,7 +204,7 @@ class StreamableHTTPClientTransport { + if (!event.event || event.event === 'message') { + try { + const message = types_js_1.JSONRPCMessageSchema.parse(JSON.parse(event.data)); +- if ((0, types_js_1.isJSONRPCResultResponse)(message)) { ++ if ((0, types_js_1.isJSONRPCResultResponse)(message) || (0, types_js_1.isJSONRPCErrorResponse)(message)) { + // Mark that we received a response - no need to reconnect for this request + receivedResponse = true; + if (replayMessageId !== undefined) { @@ -290,7 +290,38 @@ class StreamableHTTPClientTransport { this.onclose?.(); } @@ -518,10 +527,19 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88 @@ -1,5 +1,5 @@ import { createFetchWithInit, normalizeHeaders } from '../shared/transport.js'; -import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js'; -+import { isInitializedNotification, isInitializeRequest, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js'; ++import { isInitializedNotification, isInitializeRequest, isJSONRPCErrorResponse, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js'; import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js'; import { EventSourceParserStream } from 'eventsource-parser/stream'; // Default reconnection options for StreamableHTTP connections +@@ -200,7 +200,7 @@ export class StreamableHTTPClientTransport { + if (!event.event || event.event === 'message') { + try { + const message = JSONRPCMessageSchema.parse(JSON.parse(event.data)); +- if (isJSONRPCResultResponse(message)) { ++ if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) { + // Mark that we received a response - no need to reconnect for this request + receivedResponse = true; + if (replayMessageId !== undefined) { @@ -286,7 +286,38 @@ export class StreamableHTTPClientTransport { this.onclose?.(); } From 3f239b3834e7ef30a454af6830053d2e82af009f Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 30 Jul 2026 16:46:38 +0000 Subject: [PATCH 095/129] 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 a7b441535c..b1975757b4 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-fK6zAHJC3ut/KUdfqLPxVMH7Z1yv65YZ7qcHF45kPas=", - "aarch64-linux": "sha256-CSWZY/vVUCKzlwfisQQHqHjawUwL0BRc8Y9o2y/QZYA=", - "aarch64-darwin": "sha256-IyFm5NbnU63BaOO/F4/v1exz3VbvkY96yjg9iun+O9Q=", - "x86_64-darwin": "sha256-XnXFoUxqXmEx7AKx5YhaJuLY2NYMtI0ICafaenU5L6k=" + "x86_64-linux": "sha256-AD5kt1o035hdvYPyrvjiz2mcsXFJ7UJVEmbE9AJFDFs=", + "aarch64-linux": "sha256-f1gVXUpIRWC/I8dxIigdGGvPsju04MXVjZhtq5EYL64=", + "aarch64-darwin": "sha256-c+JRpjs2gsgQ2czyM6bTbYEdMbX/erOSQdX6TdrI59k=", + "x86_64-darwin": "sha256-iB7l4PtQQ9Mx/XAdtyayiLe6pZKhuXB9lpmrbTr+uWc=" } } From ceb4890ca3651899dd3e2b1564168ab098ac540d Mon Sep 17 00:00:00 2001 From: Deven Navani <4031146+devennavani@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:29:32 -0700 Subject: [PATCH 096/129] docs: add Modal provider setup (#39710) --- packages/web/src/content/docs/providers.mdx | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index d3c541311f..7931d4e03f 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -1505,6 +1505,30 @@ To use Kimi K2 from Moonshot AI: --- +### Modal + +1. Create a [shared Endpoint](https://modal.com/endpoints) for the model you want to use. + +2. Create a [proxy token](https://modal.com/docs/guide/endpoints#proxy-tokens), then join its ID and secret with a period: + + ```txt + wk-.ws- + ``` + +3. Run the `/connect` command, search for **Modal**, and enter the combined proxy token. + + ```txt + /connect + ``` + +4. Run the `/models` command to select one of the endpoints in your Modal workspace. + + ```txt + /models + ``` + +--- + ### NVIDIA NVIDIA provides access to Nemotron models and many other open models through [build.nvidia.com](https://build.nvidia.com) for free. From e024e2ef92293a81a06b6cc418422c52207ce85d Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:46:36 +1000 Subject: [PATCH 097/129] fix(app): align debug gutter text (#39782) --- packages/app/src/components/debug-bar.tsx | 54 +++++++++++++++-------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/packages/app/src/components/debug-bar.tsx b/packages/app/src/components/debug-bar.tsx index 67c9a89929..adadeda303 100644 --- a/packages/app/src/components/debug-bar.tsx +++ b/packages/app/src/components/debug-bar.tsx @@ -66,7 +66,7 @@ function Cell(props: {
- {props.label} -
-
- {props.value} +
+ {props.label} +
+
+ {props.value} +
) @@ -116,15 +124,25 @@ function FocusCell(props: { active: boolean; inline?: boolean; onClick: () => vo aria-pressed={props.active} classList={{ "flex min-w-0 items-center font-mono uppercase hover:bg-surface-raised-base focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-border-focus": true, - "min-h-[20px] w-fit flex-row justify-start gap-1.5 rounded px-1.5 py-0.5 text-left": !!props.inline, + "min-h-[20px] w-fit justify-start rounded px-1.5 py-0.5 text-left": !!props.inline, "min-h-[42px] w-full flex-col justify-center rounded-[8px] px-0.5 py-1 text-center": !props.inline, "bg-surface-raised-base text-text-strong": props.active, }} onClick={props.onClick} > - FOCUS - - {props.active ? "ON" : "OFF"} + + FOCUS + + {props.active ? "ON" : "OFF"} + ) From 7ea343cabd057e29d4f34d07d7aeae832293bfae Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Fri, 31 Jul 2026 06:24:19 +0200 Subject: [PATCH 098/129] fix(app): prevent file tree tab clipping (#39770) --- packages/app/src/pages/session/session-side-panel.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 22c52e73fe..6f074fa203 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -59,6 +59,7 @@ import { SessionFileBrowserTab, type SessionFileBrowserState } from "@/pages/ses type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff type RenderDiff = FileDiffInfo | (SnapshotFileDiff & { file: string }) | VcsFileDiff +const FILE_TREE_WIDTH_MIN = 240 function renderDiff(value: ReviewDiff): value is RenderDiff { return typeof value.file === "string" @@ -104,13 +105,14 @@ export function SessionSidePanel(props: { }), ) const open = createMemo(() => reviewOpen() || fileOpen()) + const fileTreeWidth = createMemo(() => Math.max(FILE_TREE_WIDTH_MIN, layout.fileTree.width())) const reviewTab = createMemo(() => isDesktop()) const panelWidth = createMemo(() => { if (!open()) return "0px" if (reviewOpen()) return "auto" - return `${layout.fileTree.width()}px` + return `${fileTreeWidth()}px` }) - const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px")) + const treeWidth = createMemo(() => (fileOpen() ? `${fileTreeWidth()}px` : "0px")) const diffs = createMemo(() => props.diffs().filter(renderDiff)) const diffFiles = createMemo(() => diffs().map((d) => d.file)) @@ -845,8 +847,8 @@ export function SessionSidePanel(props: { { props.size.touch() From d3f30df1806ea9f95190948c59f6e8b4c97301d0 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 04:38:34 +0000 Subject: [PATCH 099/129] fix(app): open legacy picker at home (#39804) Co-authored-by: Brendan Allan --- packages/app/src/components/dialog-select-directory.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/dialog-select-directory.tsx b/packages/app/src/components/dialog-select-directory.tsx index 2a6c877df1..96bcedb9db 100644 --- a/packages/app/src/components/dialog-select-directory.tsx +++ b/packages/app/src/components/dialog-select-directory.tsx @@ -57,9 +57,9 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { const [filter, setFilter] = createSignal("") let list: ListRef | undefined - const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory)) + const missingHome = createMemo(() => !sync.data.path.home) const [fallbackPath] = createResource( - () => (missingBase() ? true : undefined), + () => (missingHome() ? true : undefined), async (): Promise => { if ((await sdk.protocol) !== "v1") return return sdk.client.path From db4dbaa2894a1a98e90e814416eb9f7c35711819 Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Fri, 31 Jul 2026 07:14:09 +0200 Subject: [PATCH 100/129] fix(app): prevent stale session tab reads (#39767) --- .../app/src/components/session/session-header.tsx | 12 ++++++------ packages/app/src/components/titlebar-tab-nav.tsx | 5 +++-- .../app/src/pages/new-session/new-session-view.tsx | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index da1770cd13..dc6cdaa6f3 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -289,9 +289,9 @@ export function SessionHeader() { return ( <> - + {(mount) => ( - +
- + {(keybind) => ( - {keybind()} + {keybind} )} @@ -319,9 +319,9 @@ export function SessionHeader() { )} - + {(mount) => ( - +
@@ -886,9 +886,9 @@ function ProviderConnection(props: {
{language.t("provider.connect.opencodeZen.line2")}
{language.t("provider.connect.opencodeZen.visit.prefix")} - + {language.t("provider.connect.opencodeZen.visit.link")} - + {language.t("provider.connect.opencodeZen.visit.suffix")}
@@ -967,9 +967,9 @@ function ProviderConnection(props: {
{language.t("provider.connect.oauth.code.visit.prefix")} - + {language.t("provider.connect.oauth.code.visit.link")} - + {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
@@ -1006,7 +1006,9 @@ function ProviderConnection(props: {
{language.t("provider.connect.oauth.code.visit.prefix")} - {language.t("provider.connect.oauth.code.visit.link")} + + {language.t("provider.connect.oauth.code.visit.link")} + {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
@@ -1077,7 +1079,9 @@ function ProviderConnection(props: {
{language.t("provider.connect.oauth.auto.visit.prefix")} - {language.t("provider.connect.oauth.auto.visit.link")} + + {language.t("provider.connect.oauth.auto.visit.link")} + {language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })}

{language.t("provider.custom.description.prefix")} - + {language.t("provider.custom.description.link")} - + {language.t("provider.custom.description.suffix")}

diff --git a/packages/app/src/components/dialog-usage-exceeded.tsx b/packages/app/src/components/dialog-usage-exceeded.tsx index e428d4c2bb..bf5da751e2 100644 --- a/packages/app/src/components/dialog-usage-exceeded.tsx +++ b/packages/app/src/components/dialog-usage-exceeded.tsx @@ -17,7 +17,7 @@ export function DialogUsageExceeded(props: DialogGoUpsellProps) { const platform = usePlatform() const runAction = () => { - if (props.link) platform.openLink(props.link) + if (props.link) platform.openExternal(props.link) props.onClose?.() dialog.close() } diff --git a/packages/app/src/components/external-link.tsx b/packages/app/src/components/external-link.tsx new file mode 100644 index 0000000000..133e752eab --- /dev/null +++ b/packages/app/src/components/external-link.tsx @@ -0,0 +1,21 @@ +import { ComponentProps, splitProps } from "solid-js" + +export interface ExternalLinkProps extends Omit, "href"> { + href: string +} + +export function ExternalLink(props: ExternalLinkProps) { + const [local, rest] = splitProps(props, ["href", "children", "class", "target", "rel"]) + + return ( + + {local.children} + + ) +} diff --git a/packages/app/src/components/link.tsx b/packages/app/src/components/link.tsx deleted file mode 100644 index 85f7efc539..0000000000 --- a/packages/app/src/components/link.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { ComponentProps, splitProps } from "solid-js" -import { usePlatform } from "@/context/platform" - -export interface LinkProps extends Omit, "href"> { - href: string -} - -export function Link(props: LinkProps) { - const platform = usePlatform() - const [local, rest] = splitProps(props, ["href", "children", "class"]) - - return ( - { - if (!local.href) return - event.preventDefault() - platform.openLink(local.href) - }} - {...rest} - > - {local.children} - - ) -} diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index a20e66de3e..8adaa1f0fd 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -29,7 +29,7 @@ import { } from "@/context/settings" import { decode64 } from "@/utils/base64" import { playSoundById, SOUND_OPTIONS } from "@/utils/sound" -import { Link } from "./link" +import { ExternalLink } from "./external-link" import { SettingsList } from "./settings-list" let demoSoundState = { @@ -482,7 +482,7 @@ export const SettingsGeneral: Component = () => { description={ <> {language.t("settings.general.row.theme.description")}{" "} - {language.t("common.learnMore")} + {language.t("common.learnMore")} } > diff --git a/packages/app/src/components/settings-v2/general.tsx b/packages/app/src/components/settings-v2/general.tsx index e50bd316c9..b41da107ed 100644 --- a/packages/app/src/components/settings-v2/general.tsx +++ b/packages/app/src/components/settings-v2/general.tsx @@ -9,7 +9,7 @@ import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { useUpdaterAction } from "../updater-action" import { useSettings } from "@/context/settings" -import { Link } from "../link" +import { ExternalLink } from "../external-link" import { SettingsListV2 } from "./parts/list" import { SettingsRowV2 } from "./parts/row" import { LayoutRetirementNotice, LayoutTransitionToggle } from "./interface-transition" @@ -150,9 +150,9 @@ const AppearanceSection: Component<{ controller: AppearanceSettingsController }> description={ <> {language.t("settings.general.row.theme.description")}{" "} - + {language.t("common.learnMore")} - + } > diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 6503503476..7d25e7c649 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -382,7 +382,11 @@ export const Terminal = (props: TerminalProps) => { event.preventDefault() event.stopImmediatePropagation() - platform.openLink(text) + if (URL.canParse(text) && new URL(text).protocol === "file:" && platform.openLocalFile) { + platform.openLocalFile(text) + return + } + platform.openExternal(text) } onMount(() => { diff --git a/packages/app/src/components/windows-app-menu.tsx b/packages/app/src/components/windows-app-menu.tsx index 017ac4cf8d..e41c2a584a 100644 --- a/packages/app/src/components/windows-app-menu.tsx +++ b/packages/app/src/components/windows-app-menu.tsx @@ -43,7 +43,7 @@ export function WindowsAppMenu(props: { runAction(entry.action) return } - if (entry.href) props.platform.openLink(entry.href) + if (entry.href) props.platform.openExternal(entry.href) } return ( diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx index 9bfa13d854..fb562a0bf5 100644 --- a/packages/app/src/context/notification.tsx +++ b/packages/app/src/context/notification.tsx @@ -1,6 +1,6 @@ import { createStore, reconcile } from "solid-js/store" import { type Accessor, batch, createEffect, createMemo, createRoot, getOwner, onCleanup } from "solid-js" -import { useParams, useSearchParams } from "@solidjs/router" +import { useNavigate, useParams, useSearchParams } from "@solidjs/router" import { createSimpleContext } from "@opencode-ai/ui/context" import type { ServerSDK } from "./server-sdk" import type { ServerSync } from "./server-sync" @@ -118,6 +118,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi const global = useGlobal() const server = useServer() const tabs = useTabs() + const navigate = useNavigate() const platform = usePlatform() const settings = useSettings() const language = useLanguage() @@ -153,6 +154,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi platform, settings, language, + navigate, }), }), owner ?? undefined, @@ -217,6 +219,7 @@ function createServerNotificationState(input: { platform: ReturnType settings: ReturnType language: ReturnType + navigate: (href: string) => void }) { const serverSDK = () => input.sdk const serverSync = () => input.sync @@ -353,7 +356,11 @@ function createServerNotificationState(input: { const href = `/${base64Encode(directory)}/session/${sessionID}` if (settings.notifications.agent()) { - void platform.notify(language.t("notification.session.responseReady.title"), session.title ?? sessionID, href) + void platform.notify( + language.t("notification.session.responseReady.title"), + session.title ?? sessionID, + () => input.navigate(href), + ) } }) } @@ -386,7 +393,7 @@ function createServerNotificationState(input: { (typeof error === "string" ? error : language.t("notification.session.error.fallbackDescription")) const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}` if (settings.notifications.errors()) { - void platform.notify(language.t("notification.session.error.title"), description, href) + void platform.notify(language.t("notification.session.error.title"), description, () => input.navigate(href)) } }) } diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx index 4c374961a3..08686361c5 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -31,26 +31,23 @@ type PlatformBase = { /** App version */ version?: string - /** Open a URL in the default browser */ - openLink(url: string): void + /** Open a web or mail URL in the default system application */ + openExternal(url: string): void /** Open a local path in a local app (desktop only) */ openPath?(path: string, app?: string): Promise + /** Open a local file URL in its default app (desktop only) */ + openLocalFile?(url: string): void + /** Reveal a local path in the system file manager; false when the path does not exist (desktop only) */ revealPath?(path: string): Promise /** Restart the app */ restart(): Promise - /** Navigate back in history */ - back(): void - - /** Navigate forward in history */ - forward(): void - - /** Send a system notification (optional deep link) */ - notify(title: string, description?: string, href?: string): Promise + /** Send a system notification */ + notify(title: string, description?: string, onClick?: () => void): Promise /** Open a native attachment picker and read selected files sequentially (desktop only) */ openAttachmentPickerDialog?( @@ -91,9 +88,6 @@ type PlatformBase = { /** Set the preferred display backend (desktop only) */ setDisplayBackend?(backend: DisplayBackend): Promise - /** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */ - parseMarkdown?(markdown: string): Promise - /** Webview zoom level (desktop only) */ webviewZoom?: Accessor diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index c41c1bb4b6..6823d1c6d7 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -6,7 +6,6 @@ import { AppBaseProviders, AppInterface } from "@/app" import { type Platform, PlatformProvider } from "@/context/platform" import { dict as en } from "@/i18n/en" import { dict as zh } from "@/i18n/zh" -import { handleNotificationClick } from "@/utils/notification-click" import { authFromToken } from "@/utils/server" import pkg from "../package.json" import { ServerConnection } from "./context/server" @@ -54,7 +53,7 @@ const setStorage = (key: string, value: string | null) => { const readDefaultServerUrl = () => getStorage(DEFAULT_SERVER_URL_KEY) const writeDefaultServerUrl = (url: string | null) => setStorage(DEFAULT_SERVER_URL_KEY, url) -const notify: Platform["notify"] = async (title, description, href) => { +const notify: Platform["notify"] = async (title, description, onClick) => { if (!("Notification" in window)) return const permission = @@ -73,21 +72,17 @@ const notify: Platform["notify"] = async (title, description, href) => { }) notification.onclick = () => { - handleNotificationClick(href) + window.focus() + onClick?.() notification.close() } } -const openLink: Platform["openLink"] = (url) => { - window.open(url, "_blank") -} - -const back: Platform["back"] = () => { - window.history.back() -} - -const forward: Platform["forward"] = () => { - window.history.forward() +const openExternal: Platform["openExternal"] = (value) => { + if (!URL.canParse(value)) return + const url = new URL(value) + if (url.protocol !== "http:" && url.protocol !== "https:" && url.protocol !== "mailto:") return + window.open(url.href, "_blank", "noopener,noreferrer") } const restart: Platform["restart"] = async () => { @@ -122,9 +117,7 @@ const clearAuthToken = () => { const platform: Platform = { platform: "web", version: pkg.version, - openLink, - back, - forward, + openExternal, restart, notify, getDefaultServer: async () => { diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts index d18e05fc9c..2267ebf081 100644 --- a/packages/app/src/index.ts +++ b/packages/app/src/index.ts @@ -27,4 +27,3 @@ export { type WslServersState, } from "./wsl/types" export { ServerConnection } from "./context/server" -export { handleNotificationClick } from "./utils/notification-click" diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index 77c120e57b..a0b3576396 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -63,6 +63,7 @@ export function DirectoryDataProvider( navigate(href(sessionID))} onSessionHref={href} > diff --git a/packages/app/src/pages/error.tsx b/packages/app/src/pages/error.tsx index 86a997b39d..2fc6095412 100644 --- a/packages/app/src/pages/error.tsx +++ b/packages/app/src/pages/error.tsx @@ -354,7 +354,7 @@ export const ErrorPage: Component = (props) => {