From b9ea1487b3899b4695ac1f7df72b86ae71458c97 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Mar 2026 20:12:54 -0800 Subject: [PATCH 01/23] Restore Claude adapter on sibling stack Co-authored-by: codex --- .../provider/Layers/ClaudeCodeAdapter.test.ts | 930 +++++++++ .../src/provider/Layers/ClaudeCodeAdapter.ts | 1857 +++++++++++++++++ .../Layers/ProviderAdapterRegistry.test.ts | 30 +- .../Layers/ProviderAdapterRegistry.ts | 6 +- .../provider/Services/ClaudeCodeAdapter.ts | 32 + apps/server/src/serverLayers.ts | 5 + apps/web/src/routes/_chat.settings.tsx | 7 + apps/web/src/session-logic.test.ts | 6 +- apps/web/src/session-logic.ts | 2 +- 9 files changed, 2868 insertions(+), 7 deletions(-) create mode 100644 apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts create mode 100644 apps/server/src/provider/Layers/ClaudeCodeAdapter.ts create mode 100644 apps/server/src/provider/Services/ClaudeCodeAdapter.ts diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts new file mode 100644 index 000000000..cd55c3983 --- /dev/null +++ b/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts @@ -0,0 +1,930 @@ +import type { + Options as ClaudeQueryOptions, + PermissionMode, + PermissionResult, + SDKMessage, + SDKUserMessage, +} from "@anthropic-ai/claude-agent-sdk"; +import { ApprovalRequestId, ThreadId } from "@t3tools/contracts"; +import { assert, describe, it } from "@effect/vitest"; +import { Effect, Fiber, Random, Stream } from "effect"; + +import { + ProviderAdapterValidationError, +} from "../Errors.ts"; +import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts"; +import { + makeClaudeCodeAdapterLive, + type ClaudeCodeAdapterLiveOptions, +} from "./ClaudeCodeAdapter.ts"; + +class FakeClaudeQuery implements AsyncIterable { + private readonly queue: Array = []; + private readonly resolvers: Array<(value: IteratorResult) => void> = []; + private done = false; + + public readonly interruptCalls: Array = []; + public readonly setModelCalls: Array = []; + public readonly setPermissionModeCalls: Array = []; + public readonly setMaxThinkingTokensCalls: Array = []; + public closeCalls = 0; + + emit(message: SDKMessage): void { + if (this.done) { + return; + } + const resolver = this.resolvers.shift(); + if (resolver) { + resolver({ done: false, value: message }); + return; + } + this.queue.push(message); + } + + finish(): void { + if (this.done) { + return; + } + this.done = true; + for (const resolver of this.resolvers.splice(0)) { + resolver({ done: true, value: undefined }); + } + } + + readonly interrupt = async (): Promise => { + this.interruptCalls.push(undefined); + }; + + readonly setModel = async (model?: string): Promise => { + this.setModelCalls.push(model); + }; + + readonly setPermissionMode = async (mode: PermissionMode): Promise => { + this.setPermissionModeCalls.push(mode); + }; + + readonly setMaxThinkingTokens = async (maxThinkingTokens: number | null): Promise => { + this.setMaxThinkingTokensCalls.push(maxThinkingTokens); + }; + + readonly close = (): void => { + this.closeCalls += 1; + this.finish(); + }; + + [Symbol.asyncIterator](): AsyncIterator { + return { + next: () => { + if (this.queue.length > 0) { + const value = this.queue.shift(); + if (value) { + return Promise.resolve({ + done: false, + value, + }); + } + } + if (this.done) { + return Promise.resolve({ + done: true, + value: undefined, + }); + } + return new Promise((resolve) => { + this.resolvers.push(resolve); + }); + }, + }; + } +} + +interface Harness { + readonly layer: ReturnType; + readonly query: FakeClaudeQuery; + readonly getLastCreateQueryInput: () => + | { + readonly prompt: AsyncIterable; + readonly options: ClaudeQueryOptions; + } + | undefined; +} + +function makeHarness(config?: { + readonly nativeEventLogPath?: string; + readonly nativeEventLogger?: ClaudeCodeAdapterLiveOptions["nativeEventLogger"]; +}): Harness { + const query = new FakeClaudeQuery(); + let createInput: + | { + readonly prompt: AsyncIterable; + readonly options: ClaudeQueryOptions; + } + | undefined; + + const adapterOptions: ClaudeCodeAdapterLiveOptions = { + createQuery: (input) => { + createInput = input; + return query; + }, + ...(config?.nativeEventLogger + ? { + nativeEventLogger: config.nativeEventLogger, + } + : {}), + ...(config?.nativeEventLogPath + ? { + nativeEventLogPath: config.nativeEventLogPath, + } + : {}), + }; + + return { + layer: makeClaudeCodeAdapterLive(adapterOptions), + query, + getLastCreateQueryInput: () => createInput, + }; +} + +function makeDeterministicRandomService(seed = 0x1234_5678): { + nextIntUnsafe: () => number; + nextDoubleUnsafe: () => number; +} { + let state = seed >>> 0; + const nextIntUnsafe = (): number => { + state = (Math.imul(1_664_525, state) + 1_013_904_223) >>> 0; + return state; + }; + + return { + nextIntUnsafe, + nextDoubleUnsafe: () => nextIntUnsafe() / 0x1_0000_0000, + }; +} + + +const THREAD_ID = ThreadId.makeUnsafe("thread-claude-1"); +const RESUME_THREAD_ID = ThreadId.makeUnsafe("thread-claude-resume"); + +describe("ClaudeCodeAdapterLive", () => { + it.effect("returns validation error for non-claudeCode provider on startSession", () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeCodeAdapter; + const result = yield* adapter + .startSession({ threadId: THREAD_ID, provider: "codex", runtimeMode: "full-access" }) + .pipe(Effect.result); + + assert.equal(result._tag, "Failure"); + if (result._tag !== "Failure") { + return; + } + assert.deepEqual( + result.failure, + new ProviderAdapterValidationError({ + provider: "claudeCode", + operation: "startSession", + issue: "Expected provider 'claudeCode' but received 'codex'.", + }), + ); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + + it.effect("derives bypass permission mode from full-access runtime policy", () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeCodeAdapter; + yield* adapter.startSession({ + threadId: THREAD_ID, + provider: "claudeCode", + runtimeMode: "full-access", + }); + + const createInput = harness.getLastCreateQueryInput(); + assert.equal(createInput?.options.permissionMode, "bypassPermissions"); + assert.equal(createInput?.options.allowDangerouslySkipPermissions, true); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + + it.effect("keeps explicit claude permission mode over runtime-derived defaults", () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeCodeAdapter; + yield* adapter.startSession({ + threadId: THREAD_ID, + provider: "claudeCode", + runtimeMode: "full-access", + providerOptions: { + claudeCode: { + permissionMode: "plan", + }, + }, + }); + + const createInput = harness.getLastCreateQueryInput(); + assert.equal(createInput?.options.permissionMode, "plan"); + assert.equal(createInput?.options.allowDangerouslySkipPermissions, undefined); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + + it.effect("maps Claude stream/runtime messages to canonical provider runtime events", () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeCodeAdapter; + + const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 11).pipe( + Stream.runCollect, + Effect.forkChild, + ); + + const session = yield* adapter.startSession({ + threadId: THREAD_ID, + provider: "claudeCode", + model: "claude-sonnet-4-5", + runtimeMode: "full-access", + }); + + const turn = yield* adapter.sendTurn({ + threadId: session.threadId, + input: "hello", + attachments: [], + }); + + harness.query.emit({ + type: "stream_event", + session_id: "sdk-session-1", + uuid: "stream-1", + parent_tool_use_id: null, + event: { + type: "content_block_delta", + index: 0, + delta: { + type: "text_delta", + text: "Hi", + }, + }, + } as unknown as SDKMessage); + + harness.query.emit({ + type: "stream_event", + session_id: "sdk-session-1", + uuid: "stream-2", + parent_tool_use_id: null, + event: { + type: "content_block_start", + index: 1, + content_block: { + type: "tool_use", + id: "tool-1", + name: "Bash", + input: { + command: "ls", + }, + }, + }, + } as unknown as SDKMessage); + + harness.query.emit({ + type: "stream_event", + session_id: "sdk-session-1", + uuid: "stream-3", + parent_tool_use_id: null, + event: { + type: "content_block_stop", + index: 1, + }, + } as unknown as SDKMessage); + + harness.query.emit({ + type: "assistant", + session_id: "sdk-session-1", + uuid: "assistant-1", + parent_tool_use_id: null, + message: { + id: "assistant-message-1", + content: [{ type: "text", text: "Hi" }], + }, + } as unknown as SDKMessage); + + harness.query.emit({ + type: "result", + subtype: "success", + is_error: false, + errors: [], + session_id: "sdk-session-1", + uuid: "result-1", + } as unknown as SDKMessage); + + const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); + assert.deepEqual( + runtimeEvents.map((event) => event.type), + [ + "session.started", + "session.configured", + "session.state.changed", + "turn.started", + "thread.started", + "content.delta", + "item.started", + "item.completed", + "item.updated", + "item.completed", + "turn.completed", + ], + ); + + const turnStarted = runtimeEvents[3]; + assert.equal(turnStarted?.type, "turn.started"); + if (turnStarted?.type === "turn.started") { + assert.equal(String(turnStarted.turnId), String(turn.turnId)); + } + + const deltaEvent = runtimeEvents.find((event) => event.type === "content.delta"); + assert.equal(deltaEvent?.type, "content.delta"); + if (deltaEvent?.type === "content.delta") { + assert.equal(deltaEvent.payload.delta, "Hi"); + assert.equal(String(deltaEvent.turnId), String(turn.turnId)); + } + + const toolStarted = runtimeEvents.find((event) => event.type === "item.started"); + assert.equal(toolStarted?.type, "item.started"); + if (toolStarted?.type === "item.started") { + assert.equal(toolStarted.payload.itemType, "command_execution"); + } + + const turnCompleted = runtimeEvents[runtimeEvents.length - 1]; + assert.equal(turnCompleted?.type, "turn.completed"); + if (turnCompleted?.type === "turn.completed") { + assert.equal(String(turnCompleted.turnId), String(turn.turnId)); + assert.equal(turnCompleted.payload.state, "completed"); + } + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + + it.effect("emits completion only after turn result when assistant frames arrive before deltas", () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeCodeAdapter; + + const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 9).pipe( + Stream.runCollect, + Effect.forkChild, + ); + + const session = yield* adapter.startSession({ + threadId: THREAD_ID, + provider: "claudeCode", + runtimeMode: "full-access", + }); + + const turn = yield* adapter.sendTurn({ + threadId: session.threadId, + input: "hello", + attachments: [], + }); + + harness.query.emit({ + type: "assistant", + session_id: "sdk-session-early-assistant", + uuid: "assistant-early", + parent_tool_use_id: null, + message: { + id: "assistant-message-early", + content: [{ type: "tool_use", id: "tool-early", name: "Read", input: { path: "a.ts" } }], + }, + } as unknown as SDKMessage); + + harness.query.emit({ + type: "stream_event", + session_id: "sdk-session-early-assistant", + uuid: "stream-early", + parent_tool_use_id: null, + event: { + type: "content_block_delta", + index: 0, + delta: { + type: "text_delta", + text: "Late text", + }, + }, + } as unknown as SDKMessage); + + harness.query.emit({ + type: "result", + subtype: "success", + is_error: false, + errors: [], + session_id: "sdk-session-early-assistant", + uuid: "result-early", + } as unknown as SDKMessage); + + const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); + assert.deepEqual( + runtimeEvents.map((event) => event.type), + [ + "session.started", + "session.configured", + "session.state.changed", + "turn.started", + "thread.started", + "item.updated", + "content.delta", + "item.completed", + "turn.completed", + ], + ); + + const deltaIndex = runtimeEvents.findIndex((event) => event.type === "content.delta"); + const completedIndex = runtimeEvents.findIndex((event) => event.type === "item.completed"); + assert.equal(deltaIndex >= 0 && completedIndex >= 0 && deltaIndex < completedIndex, true); + + const deltaEvent = runtimeEvents[deltaIndex]; + assert.equal(deltaEvent?.type, "content.delta"); + if (deltaEvent?.type === "content.delta") { + assert.equal(deltaEvent.payload.delta, "Late text"); + assert.equal(String(deltaEvent.turnId), String(turn.turnId)); + } + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + + it.effect("falls back to assistant payload text when stream deltas are absent", () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeCodeAdapter; + + const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 9).pipe( + Stream.runCollect, + Effect.forkChild, + ); + + const session = yield* adapter.startSession({ + threadId: THREAD_ID, + provider: "claudeCode", + runtimeMode: "full-access", + }); + + const turn = yield* adapter.sendTurn({ + threadId: session.threadId, + input: "hello", + attachments: [], + }); + + harness.query.emit({ + type: "assistant", + session_id: "sdk-session-fallback-text", + uuid: "assistant-fallback", + parent_tool_use_id: null, + message: { + id: "assistant-message-fallback", + content: [{ type: "text", text: "Fallback hello" }], + }, + } as unknown as SDKMessage); + + harness.query.emit({ + type: "result", + subtype: "success", + is_error: false, + errors: [], + session_id: "sdk-session-fallback-text", + uuid: "result-fallback", + } as unknown as SDKMessage); + + const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); + assert.deepEqual( + runtimeEvents.map((event) => event.type), + [ + "session.started", + "session.configured", + "session.state.changed", + "turn.started", + "thread.started", + "item.updated", + "content.delta", + "item.completed", + "turn.completed", + ], + ); + + const deltaEvent = runtimeEvents.find((event) => event.type === "content.delta"); + assert.equal(deltaEvent?.type, "content.delta"); + if (deltaEvent?.type === "content.delta") { + assert.equal(deltaEvent.payload.delta, "Fallback hello"); + assert.equal(String(deltaEvent.turnId), String(turn.turnId)); + } + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + + it.effect("does not fabricate provider thread ids before first SDK session_id", () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeCodeAdapter; + + const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 5).pipe( + Stream.runCollect, + Effect.forkChild, + ); + + const session = yield* adapter.startSession({ + threadId: THREAD_ID, + provider: "claudeCode", + runtimeMode: "full-access", + }); + assert.equal(session.threadId, undefined); + + const turn = yield* adapter.sendTurn({ + threadId: session.threadId, + input: "hello", + attachments: [], + }); + assert.equal(turn.threadId, undefined); + + harness.query.emit({ + type: "stream_event", + session_id: "sdk-thread-real", + uuid: "stream-thread-real", + parent_tool_use_id: null, + event: { + type: "message_start", + message: { + id: "msg-thread-real", + }, + }, + } as unknown as SDKMessage); + + harness.query.emit({ + type: "result", + subtype: "success", + is_error: false, + errors: [], + session_id: "sdk-thread-real", + uuid: "result-thread-real", + } as unknown as SDKMessage); + + const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); + assert.deepEqual( + runtimeEvents.map((event) => event.type), + [ + "session.started", + "session.configured", + "session.state.changed", + "turn.started", + "thread.started", + ], + ); + + const sessionStarted = runtimeEvents[0]; + assert.equal(sessionStarted?.type, "session.started"); + if (sessionStarted?.type === "session.started") { + assert.equal("threadId" in sessionStarted, false); + } + + const threadStarted = runtimeEvents[4]; + assert.equal(threadStarted?.type, "thread.started"); + if (threadStarted?.type === "thread.started") { + assert.equal(threadStarted.threadId, "sdk-thread-real"); + } + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + + it.effect("bridges approval request/response lifecycle through canUseTool", () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeCodeAdapter; + + const session = yield* adapter.startSession({ + threadId: THREAD_ID, + provider: "claudeCode", + runtimeMode: "approval-required", + }); + + yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); + + const createInput = harness.getLastCreateQueryInput(); + const canUseTool = createInput?.options.canUseTool; + assert.equal(typeof canUseTool, "function"); + if (!canUseTool) { + return; + } + + const permissionPromise = canUseTool( + "Bash", + { command: "pwd" }, + { + signal: new AbortController().signal, + suggestions: [ + { + type: "setMode", + mode: "default", + destination: "session", + }, + ], + toolUseID: "tool-use-1", + }, + ); + + const requested = yield* Stream.runHead(adapter.streamEvents); + assert.equal(requested._tag, "Some"); + if (requested._tag !== "Some") { + return; + } + assert.equal(requested.value.type, "request.opened"); + if (requested.value.type !== "request.opened") { + return; + } + const runtimeRequestId = requested.value.requestId; + assert.equal(typeof runtimeRequestId, "string"); + if (runtimeRequestId === undefined) { + return; + } + + yield* adapter.respondToRequest( + session.threadId, + ApprovalRequestId.makeUnsafe(runtimeRequestId), + "accept", + ); + + const resolved = yield* Stream.runHead(adapter.streamEvents); + assert.equal(resolved._tag, "Some"); + if (resolved._tag !== "Some") { + return; + } + assert.equal(resolved.value.type, "request.resolved"); + if (resolved.value.type !== "request.resolved") { + return; + } + assert.equal(resolved.value.requestId, requested.value.requestId); + assert.equal(resolved.value.payload.decision, "accept"); + + const permissionResult = yield* Effect.promise(() => permissionPromise); + assert.equal((permissionResult as PermissionResult).behavior, "allow"); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + + it.effect("passes parsed resume cursor values to Claude query options", () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeCodeAdapter; + + const session = yield* adapter.startSession({ + threadId: RESUME_THREAD_ID, + provider: "claudeCode", + resumeCursor: { + threadId: "resume-thread-1", + resume: "550e8400-e29b-41d4-a716-446655440000", + resumeSessionAt: "assistant-99", + turnCount: 3, + }, + runtimeMode: "full-access", + }); + + assert.equal(session.threadId, "resume-thread-1"); + assert.deepEqual(session.resumeCursor, { + threadId: "resume-thread-1", + resume: "550e8400-e29b-41d4-a716-446655440000", + resumeSessionAt: "assistant-99", + turnCount: 3, + }); + + const createInput = harness.getLastCreateQueryInput(); + assert.equal(createInput?.options.resume, "550e8400-e29b-41d4-a716-446655440000"); + assert.equal(createInput?.options.resumeSessionAt, "assistant-99"); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + + it.effect("does not synthesize resume session id from generated thread ids", () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeCodeAdapter; + + const session = yield* adapter.startSession({ + threadId: THREAD_ID, + provider: "claudeCode", + runtimeMode: "full-access", + }); + + assert.equal( + "resume" in (session.resumeCursor as Record), + false, + ); + + const createInput = harness.getLastCreateQueryInput(); + assert.equal(createInput?.options.resume, undefined); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + + it.effect("supports rollbackThread by trimming in-memory turns and preserving earlier turns", () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeCodeAdapter; + + const session = yield* adapter.startSession({ + threadId: THREAD_ID, + provider: "claudeCode", + runtimeMode: "full-access", + }); + + const firstTurn = yield* adapter.sendTurn({ + threadId: session.threadId, + input: "first", + attachments: [], + }); + + const firstCompletedFiber = yield* Stream.filter(adapter.streamEvents, (event) => event.type === "turn.completed").pipe( + Stream.runHead, + Effect.forkChild, + ); + + harness.query.emit({ + type: "result", + subtype: "success", + is_error: false, + errors: [], + session_id: "sdk-session-rollback", + uuid: "result-first", + } as unknown as SDKMessage); + + const firstCompleted = yield* Fiber.join(firstCompletedFiber); + assert.equal(firstCompleted._tag, "Some"); + if (firstCompleted._tag === "Some" && firstCompleted.value.type === "turn.completed") { + assert.equal(String(firstCompleted.value.turnId), String(firstTurn.turnId)); + } + + const secondTurn = yield* adapter.sendTurn({ + threadId: session.threadId, + input: "second", + attachments: [], + }); + + const secondCompletedFiber = yield* Stream.filter(adapter.streamEvents, (event) => event.type === "turn.completed").pipe( + Stream.runHead, + Effect.forkChild, + ); + + harness.query.emit({ + type: "result", + subtype: "success", + is_error: false, + errors: [], + session_id: "sdk-session-rollback", + uuid: "result-second", + } as unknown as SDKMessage); + + const secondCompleted = yield* Fiber.join(secondCompletedFiber); + assert.equal(secondCompleted._tag, "Some"); + if (secondCompleted._tag === "Some" && secondCompleted.value.type === "turn.completed") { + assert.equal(String(secondCompleted.value.turnId), String(secondTurn.turnId)); + } + + const threadBeforeRollback = yield* adapter.readThread(session.threadId); + assert.equal(threadBeforeRollback.turns.length, 2); + + const rolledBack = yield* adapter.rollbackThread(session.threadId, 1); + assert.equal(rolledBack.turns.length, 1); + assert.equal(rolledBack.turns[0]?.id, firstTurn.turnId); + + const threadAfterRollback = yield* adapter.readThread(session.threadId); + assert.equal(threadAfterRollback.turns.length, 1); + assert.equal(threadAfterRollback.turns[0]?.id, firstTurn.turnId); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + + it.effect("updates model on sendTurn when model override is provided", () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeCodeAdapter; + + const session = yield* adapter.startSession({ + threadId: THREAD_ID, + provider: "claudeCode", + runtimeMode: "full-access", + }); + yield* adapter.sendTurn({ + threadId: session.threadId, + input: "hello", + model: "claude-opus-4-6", + attachments: [], + }); + + assert.deepEqual(harness.query.setModelCalls, ["claude-opus-4-6"]); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); + + it.effect("writes provider-native observability records when enabled", () => { + const nativeEvents: Array<{ + event?: { + provider?: string; + method?: string; + threadId?: string; + turnId?: string; + }; + }> = []; + const harness = makeHarness({ + nativeEventLogger: { + filePath: "memory://claude-native-events", + write: (event) => { + nativeEvents.push(event as (typeof nativeEvents)[number]); + return Effect.void; + }, + close: () => Effect.void, + }, + }); + return Effect.gen(function* () { + const adapter = yield* ClaudeCodeAdapter; + + const session = yield* adapter.startSession({ + threadId: THREAD_ID, + provider: "claudeCode", + runtimeMode: "full-access", + }); + const turn = yield* adapter.sendTurn({ + threadId: session.threadId, + input: "hello", + attachments: [], + }); + + const turnCompletedFiber = yield* Stream.filter( + adapter.streamEvents, + (event) => event.type === "turn.completed", + ).pipe(Stream.runHead, Effect.forkChild); + + harness.query.emit({ + type: "stream_event", + session_id: "sdk-session-native-log", + uuid: "stream-native-log", + parent_tool_use_id: null, + event: { + type: "content_block_delta", + index: 0, + delta: { + type: "text_delta", + text: "hi", + }, + }, + } as unknown as SDKMessage); + + harness.query.emit({ + type: "result", + subtype: "success", + is_error: false, + errors: [], + session_id: "sdk-session-native-log", + uuid: "result-native-log", + } as unknown as SDKMessage); + + const turnCompleted = yield* Fiber.join(turnCompletedFiber); + assert.equal(turnCompleted._tag, "Some"); + + assert.equal(nativeEvents.length > 0, true); + assert.equal(nativeEvents.some((record) => record.event?.provider === "claudeCode"), true); + assert.equal(nativeEvents.some((record) => String(record.event?.threadId) === String(session.threadId)), true); + assert.equal( + nativeEvents.some((record) => String(record.event?.turnId) === String(turn.turnId)), + true, + ); + assert.equal( + nativeEvents.some( + (record) => record.event?.method === "claude/stream_event/content_block_delta/text_delta", + ), + true, + ); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }); +}); diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts new file mode 100644 index 000000000..e6bccec4e --- /dev/null +++ b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts @@ -0,0 +1,1857 @@ +/** + * ClaudeCodeAdapterLive - Scoped live implementation for the Claude Code provider adapter. + * + * Wraps `@anthropic-ai/claude-agent-sdk` query sessions behind the generic + * provider adapter contract and emits canonical runtime events. + * + * @module ClaudeCodeAdapterLive + */ +import { + type CanUseTool, + query, + type Options as ClaudeQueryOptions, + type PermissionMode, + type PermissionResult, + type PermissionUpdate, + type SDKMessage, + type SDKResultMessage, + type SDKUserMessage, +} from "@anthropic-ai/claude-agent-sdk"; +import { + ApprovalRequestId, + type CanonicalItemType, + type CanonicalRequestType, + EventId, + type ProviderApprovalDecision, + ProviderItemId, + type ProviderRuntimeEvent, + type ProviderRuntimeTurnStatus, + type ProviderSendTurnInput, + type ProviderSession, + RuntimeItemId, + RuntimeRequestId, + RuntimeTaskId, + ThreadId, + TurnId, +} from "@t3tools/contracts"; +import { Cause, DateTime, Deferred, Effect, Layer, Queue, Random, Ref, Stream } from "effect"; + +import { + ProviderAdapterProcessError, + ProviderAdapterRequestError, + ProviderAdapterSessionClosedError, + ProviderAdapterSessionNotFoundError, + ProviderAdapterValidationError, + type ProviderAdapterError, +} from "../Errors.ts"; +import { ClaudeCodeAdapter, type ClaudeCodeAdapterShape } from "../Services/ClaudeCodeAdapter.ts"; +import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; + +const PROVIDER = "claudeCode" as const; + +type PromptQueueItem = + | { + readonly type: "message"; + readonly message: SDKUserMessage; + } + | { + readonly type: "terminate"; + }; + +interface ClaudeResumeState { + readonly threadId?: ThreadId; + readonly resume?: string; + readonly resumeSessionAt?: string; + readonly turnCount?: number; +} + +interface ClaudeTurnState { + readonly turnId: TurnId; + readonly assistantItemId: string; + readonly startedAt: string; + readonly items: Array; + readonly messageCompleted: boolean; + readonly emittedTextDelta: boolean; + readonly fallbackAssistantText: string; +} + +interface PendingApproval { + readonly requestType: CanonicalRequestType; + readonly detail?: string; + readonly suggestions?: ReadonlyArray; + readonly decision: Deferred.Deferred; +} + +interface ToolInFlight { + readonly itemId: string; + readonly itemType: CanonicalItemType; + readonly toolName: string; + readonly title: string; + readonly detail?: string; +} + +interface ClaudeSessionContext { + session: ProviderSession; + readonly promptQueue: Queue.Queue; + readonly query: ClaudeQueryRuntime; + readonly startedAt: string; + resumeSessionId: string | undefined; + readonly pendingApprovals: Map; + readonly turns: Array<{ + id: TurnId; + items: Array; + }>; + readonly inFlightTools: Map; + turnState: ClaudeTurnState | undefined; + lastAssistantUuid: string | undefined; + lastThreadStartedId: string | undefined; + stopped: boolean; +} + +interface ClaudeQueryRuntime extends AsyncIterable { + readonly interrupt: () => Promise; + readonly setModel: (model?: string) => Promise; + readonly setPermissionMode: (mode: PermissionMode) => Promise; + readonly setMaxThinkingTokens: (maxThinkingTokens: number | null) => Promise; + readonly close: () => void; +} + +export interface ClaudeCodeAdapterLiveOptions { + readonly createQuery?: (input: { + readonly prompt: AsyncIterable; + readonly options: ClaudeQueryOptions; + }) => ClaudeQueryRuntime; + readonly nativeEventLogPath?: string; + readonly nativeEventLogger?: EventNdjsonLogger; +} + +function isUuid(value: string): boolean { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); +} + +function isSyntheticClaudeThreadId(value: string): boolean { + return value.startsWith("claude-thread-"); +} + +function toMessage(cause: unknown, fallback: string): string { + if (cause instanceof Error && cause.message.length > 0) { + return cause.message; + } + return fallback; +} + +function asRuntimeItemId(value: string): RuntimeItemId { + return RuntimeItemId.makeUnsafe(value); +} + +function asCanonicalTurnId(value: TurnId): TurnId { + return value; +} + +function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId { + return RuntimeRequestId.makeUnsafe(value); +} + +function toPermissionMode(value: unknown): PermissionMode | undefined { + switch (value) { + case "default": + case "acceptEdits": + case "bypassPermissions": + case "plan": + case "dontAsk": + return value; + default: + return undefined; + } +} + +function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undefined { + if (!resumeCursor || typeof resumeCursor !== "object") { + return undefined; + } + const cursor = resumeCursor as { + threadId?: unknown; + resume?: unknown; + sessionId?: unknown; + resumeSessionAt?: unknown; + turnCount?: unknown; + }; + + const threadIdCandidate = typeof cursor.threadId === "string" ? cursor.threadId : undefined; + const threadId = + threadIdCandidate && !isSyntheticClaudeThreadId(threadIdCandidate) + ? ThreadId.makeUnsafe(threadIdCandidate) + : undefined; + const resumeCandidate = + typeof cursor.resume === "string" + ? cursor.resume + : typeof cursor.sessionId === "string" + ? cursor.sessionId + : undefined; + const resume = resumeCandidate && isUuid(resumeCandidate) ? resumeCandidate : undefined; + const resumeSessionAt = + typeof cursor.resumeSessionAt === "string" ? cursor.resumeSessionAt : undefined; + const turnCountValue = typeof cursor.turnCount === "number" ? cursor.turnCount : undefined; + + return { + ...(threadId ? { threadId } : {}), + ...(resume ? { resume } : {}), + ...(resumeSessionAt ? { resumeSessionAt } : {}), + ...(turnCountValue !== undefined && Number.isInteger(turnCountValue) && turnCountValue >= 0 + ? { turnCount: turnCountValue } + : {}), + }; +} + +function classifyToolItemType(toolName: string): CanonicalItemType { + const normalized = toolName.toLowerCase(); + if ( + normalized.includes("bash") || + normalized.includes("command") || + normalized.includes("shell") || + normalized.includes("terminal") + ) { + return "command_execution"; + } + if ( + normalized.includes("edit") || + normalized.includes("write") || + normalized.includes("file") || + normalized.includes("patch") || + normalized.includes("replace") || + normalized.includes("create") || + normalized.includes("delete") + ) { + return "file_change"; + } + if (normalized.includes("mcp")) { + return "mcp_tool_call"; + } + return "dynamic_tool_call"; +} + +function classifyRequestType(toolName: string): CanonicalRequestType { + const normalized = toolName.toLowerCase(); + if (normalized === "read" || normalized.includes("read file") || normalized.includes("view")) { + return "file_read_approval"; + } + return classifyToolItemType(toolName) === "command_execution" + ? "command_execution_approval" + : "file_change_approval"; +} + +function summarizeToolRequest(toolName: string, input: Record): string { + const commandValue = input.command ?? input.cmd; + const command = typeof commandValue === "string" ? commandValue : undefined; + if (command && command.trim().length > 0) { + return `${toolName}: ${command.trim().slice(0, 400)}`; + } + + const serialized = JSON.stringify(input); + if (serialized.length <= 400) { + return `${toolName}: ${serialized}`; + } + return `${toolName}: ${serialized.slice(0, 397)}...`; +} + +function titleForTool(itemType: CanonicalItemType): string { + switch (itemType) { + case "command_execution": + return "Command run"; + case "file_change": + return "File change"; + case "mcp_tool_call": + return "MCP tool call"; + case "dynamic_tool_call": + return "Tool call"; + default: + return "Item"; + } +} + +function buildUserMessage(input: ProviderSendTurnInput): SDKUserMessage { + const fragments: string[] = []; + + if (input.input && input.input.trim().length > 0) { + fragments.push(input.input.trim()); + } + + for (const attachment of input.attachments ?? []) { + if (attachment.type === "image") { + fragments.push( + `Attached image: ${attachment.name} (${attachment.mimeType}, ${attachment.sizeBytes} bytes).`, + ); + } + } + + const text = fragments.join("\n\n"); + + return { + type: "user", + session_id: "", + parent_tool_use_id: null, + message: { + role: "user", + content: [{ type: "text", text }], + }, + } as SDKUserMessage; +} + +function turnStatusFromResult(result: SDKResultMessage): ProviderRuntimeTurnStatus { + if (result.subtype === "success") { + return "completed"; + } + + const errors = result.errors.join(" ").toLowerCase(); + if (errors.includes("interrupt")) { + return "interrupted"; + } + if (errors.includes("cancel")) { + return "cancelled"; + } + return "failed"; +} + +function streamKindFromDeltaType(deltaType: string): "assistant_text" | "reasoning_text" { + return deltaType.includes("thinking") ? "reasoning_text" : "assistant_text"; +} + +function providerThreadRef( + context: ClaudeSessionContext, +): { readonly providerThreadId: string } | {} { + return context.resumeSessionId ? { providerThreadId: context.resumeSessionId } : {}; +} + +function extractAssistantText(message: SDKMessage): string { + if (message.type !== "assistant") { + return ""; + } + + const content = (message.message as { content?: unknown } | undefined)?.content; + if (!Array.isArray(content)) { + return ""; + } + + const fragments: string[] = []; + for (const block of content) { + if (!block || typeof block !== "object") { + continue; + } + const candidate = block as { type?: unknown; text?: unknown }; + if ( + candidate.type === "text" && + typeof candidate.text === "string" && + candidate.text.length > 0 + ) { + fragments.push(candidate.text); + } + } + + return fragments.join(""); +} + +function toSessionError( + threadId: ThreadId, + cause: unknown, +): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined { + const normalized = toMessage(cause, "").toLowerCase(); + if (normalized.includes("unknown session") || normalized.includes("not found")) { + return new ProviderAdapterSessionNotFoundError({ + provider: PROVIDER, + threadId, + cause, + }); + } + if (normalized.includes("closed")) { + return new ProviderAdapterSessionClosedError({ + provider: PROVIDER, + threadId, + cause, + }); + } + return undefined; +} + +function toRequestError( + threadId: ThreadId, + method: string, + cause: unknown, +): ProviderAdapterError { + const sessionError = toSessionError(threadId, cause); + if (sessionError) { + return sessionError; + } + return new ProviderAdapterRequestError({ + provider: PROVIDER, + method, + detail: toMessage(cause, `${method} failed`), + cause, + }); +} + +function sdkMessageType(value: unknown): string | undefined { + if (!value || typeof value !== "object") { + return undefined; + } + const record = value as { type?: unknown }; + return typeof record.type === "string" ? record.type : undefined; +} + +function sdkMessageSubtype(value: unknown): string | undefined { + if (!value || typeof value !== "object") { + return undefined; + } + const record = value as { subtype?: unknown }; + return typeof record.subtype === "string" ? record.subtype : undefined; +} + +function sdkNativeMethod(message: SDKMessage): string { + const subtype = sdkMessageSubtype(message); + if (subtype) { + return `claude/${message.type}/${subtype}`; + } + + if (message.type === "stream_event") { + const streamType = sdkMessageType(message.event); + if (streamType) { + const deltaType = + streamType === "content_block_delta" + ? sdkMessageType((message.event as { delta?: unknown }).delta) + : undefined; + if (deltaType) { + return `claude/${message.type}/${streamType}/${deltaType}`; + } + return `claude/${message.type}/${streamType}`; + } + } + + return `claude/${message.type}`; +} + +function sdkNativeItemId(message: SDKMessage): string | undefined { + if (message.type === "assistant") { + const maybeId = (message.message as { id?: unknown }).id; + if (typeof maybeId === "string") { + return maybeId; + } + return undefined; + } + + if (message.type === "stream_event") { + const event = message.event as { + type?: unknown; + content_block?: { id?: unknown }; + }; + if (event.type === "content_block_start" && typeof event.content_block?.id === "string") { + return event.content_block.id; + } + } + + return undefined; +} + +function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { + return Effect.gen(function* () { + const nativeEventLogger = + options?.nativeEventLogger ?? + (options?.nativeEventLogPath !== undefined + ? yield* makeEventNdjsonLogger(options.nativeEventLogPath, { + stream: "native", + }) + : undefined); + + const createQuery = + options?.createQuery ?? + ((input: { + readonly prompt: AsyncIterable; + readonly options: ClaudeQueryOptions; + }) => query({ prompt: input.prompt, options: input.options }) as ClaudeQueryRuntime); + + const sessions = new Map(); + const runtimeEventQueue = yield* Queue.unbounded(); + + const nowIso = Effect.map(DateTime.now, DateTime.formatIso); + const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.makeUnsafe(id)); + const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); + + const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect => + Queue.offer(runtimeEventQueue, event).pipe(Effect.asVoid); + + const logNativeSdkMessage = ( + context: ClaudeSessionContext, + message: SDKMessage, + ): Effect.Effect => + Effect.gen(function* () { + if (!nativeEventLogger) { + return; + } + + const observedAt = new Date().toISOString(); + const itemId = sdkNativeItemId(message); + + yield* nativeEventLogger + .write( + { + observedAt, + event: { + id: + "uuid" in message && typeof message.uuid === "string" + ? message.uuid + : crypto.randomUUID(), + kind: "notification", + provider: PROVIDER, + createdAt: observedAt, + method: sdkNativeMethod(message), + ...(typeof message.session_id === "string" + ? { providerThreadId: message.session_id } + : {}), + ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), + ...(itemId ? { itemId: ProviderItemId.makeUnsafe(itemId) } : {}), + payload: message, + }, + }, + null, + ); + }); + + const snapshotThread = ( + context: ClaudeSessionContext, + ): Effect.Effect<{ + threadId: ThreadId; + turns: ReadonlyArray<{ + id: TurnId; + items: ReadonlyArray; + }>; + }, ProviderAdapterValidationError> => + Effect.gen(function* () { + const threadId = context.session.threadId; + if (!threadId) { + return yield* new ProviderAdapterValidationError({ + provider: PROVIDER, + operation: "readThread", + issue: "Session thread id is not initialized yet.", + }); + } + return { + threadId, + turns: context.turns.map((turn) => ({ + id: turn.id, + items: [...turn.items], + })), + }; + }); + + const updateResumeCursor = (context: ClaudeSessionContext): Effect.Effect => + Effect.gen(function* () { + const threadId = context.session.threadId; + if (!threadId) return; + + const resumeCursor = { + threadId, + ...(context.resumeSessionId ? { resume: context.resumeSessionId } : {}), + ...(context.lastAssistantUuid ? { resumeSessionAt: context.lastAssistantUuid } : {}), + turnCount: context.turns.length, + }; + + context.session = { + ...context.session, + resumeCursor, + updatedAt: yield* nowIso, + }; + }); + + const ensureThreadId = ( + context: ClaudeSessionContext, + message: SDKMessage, + ): Effect.Effect => + Effect.gen(function* () { + if (typeof message.session_id !== "string" || message.session_id.length === 0) { + return; + } + const nextThreadId = message.session_id; + context.resumeSessionId = message.session_id; + yield* updateResumeCursor(context); + + if (context.lastThreadStartedId !== nextThreadId) { + context.lastThreadStartedId = nextThreadId; + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "thread.started", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + payload: { + providerThreadId: nextThreadId, + }, + providerRefs: {}, + raw: { + source: "claude.sdk.message", + method: "claude/thread/started", + payload: { + session_id: message.session_id, + }, + }, + }); + } + }); + + const emitRuntimeError = ( + context: ClaudeSessionContext, + message: string, + cause?: unknown, + ): Effect.Effect => + Effect.gen(function* () { + if (cause !== undefined) { + void cause; + } + const turnState = context.turnState; + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "runtime.error", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}), + payload: { + message, + class: "provider_error", + ...(cause !== undefined ? { detail: cause } : {}), + }, + providerRefs: { + ...providerThreadRef(context), + ...(turnState ? { providerTurnId: String(turnState.turnId) } : {}), + }, + }); + }); + + const emitRuntimeWarning = ( + context: ClaudeSessionContext, + message: string, + detail?: unknown, + ): Effect.Effect => + Effect.gen(function* () { + const turnState = context.turnState; + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "runtime.warning", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}), + payload: { + message, + ...(detail !== undefined ? { detail } : {}), + }, + providerRefs: { + ...providerThreadRef(context), + ...(turnState ? { providerTurnId: String(turnState.turnId) } : {}), + }, + }); + }); + + const completeTurn = ( + context: ClaudeSessionContext, + status: ProviderRuntimeTurnStatus, + errorMessage?: string, + result?: SDKResultMessage, + ): Effect.Effect => + Effect.gen(function* () { + const turnState = context.turnState; + if (!turnState) { + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "turn.completed", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + payload: { + state: status, + ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), + ...(result?.usage ? { usage: result.usage } : {}), + ...(result?.modelUsage ? { modelUsage: result.modelUsage } : {}), + ...(typeof result?.total_cost_usd === "number" + ? { totalCostUsd: result.total_cost_usd } + : {}), + ...(errorMessage ? { errorMessage } : {}), + }, + providerRefs: {}, + }); + return; + } + + if (!turnState.messageCompleted) { + if (!turnState.emittedTextDelta && turnState.fallbackAssistantText.length > 0) { + const deltaStamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "content.delta", + eventId: deltaStamp.eventId, + provider: PROVIDER, + createdAt: deltaStamp.createdAt, + threadId: context.session.threadId, + turnId: turnState.turnId, + itemId: asRuntimeItemId(turnState.assistantItemId), + payload: { + streamKind: "assistant_text", + delta: turnState.fallbackAssistantText, + }, + providerRefs: { + ...providerThreadRef(context), + providerTurnId: String(turnState.turnId), + providerItemId: ProviderItemId.makeUnsafe(turnState.assistantItemId), + }, + }); + } + + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "item.completed", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + itemId: asRuntimeItemId(turnState.assistantItemId), + threadId: context.session.threadId, + turnId: turnState.turnId, + payload: { + itemType: "assistant_message", + status: "completed", + title: "Assistant message", + }, + providerRefs: { + ...providerThreadRef(context), + providerTurnId: turnState.turnId, + providerItemId: ProviderItemId.makeUnsafe(turnState.assistantItemId), + }, + }); + } + + context.turns.push({ + id: turnState.turnId, + items: [...turnState.items], + }); + + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "turn.completed", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + turnId: turnState.turnId, + payload: { + state: status, + ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), + ...(result?.usage ? { usage: result.usage } : {}), + ...(result?.modelUsage ? { modelUsage: result.modelUsage } : {}), + ...(typeof result?.total_cost_usd === "number" + ? { totalCostUsd: result.total_cost_usd } + : {}), + ...(errorMessage ? { errorMessage } : {}), + }, + providerRefs: { + ...providerThreadRef(context), + providerTurnId: turnState.turnId, + }, + }); + + const updatedAt = yield* nowIso; + context.turnState = undefined; + context.session = { + ...context.session, + status: "ready", + activeTurnId: undefined, + updatedAt, + ...(status === "failed" && errorMessage ? { lastError: errorMessage } : {}), + }; + yield* updateResumeCursor(context); + }); + + const handleStreamEvent = ( + context: ClaudeSessionContext, + message: SDKMessage, + ): Effect.Effect => + Effect.gen(function* () { + if (message.type !== "stream_event") { + return; + } + + const { event } = message; + + if (event.type === "content_block_delta") { + if ( + event.delta.type === "text_delta" && + event.delta.text.length > 0 && + context.turnState + ) { + if (!context.turnState.emittedTextDelta) { + context.turnState = { + ...context.turnState, + emittedTextDelta: true, + }; + } + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "content.delta", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + turnId: context.turnState.turnId, + itemId: asRuntimeItemId(context.turnState.assistantItemId), + payload: { + streamKind: streamKindFromDeltaType(event.delta.type), + delta: event.delta.text, + }, + providerRefs: { + ...providerThreadRef(context), + providerTurnId: context.turnState.turnId, + providerItemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId), + }, + raw: { + source: "claude.sdk.message", + method: "claude/stream_event/content_block_delta", + payload: message, + }, + }); + } + return; + } + + if (event.type === "content_block_start") { + const { index, content_block: block } = event; + if ( + block.type !== "tool_use" && + block.type !== "server_tool_use" && + block.type !== "mcp_tool_use" + ) { + return; + } + + const toolName = block.name; + const itemType = classifyToolItemType(toolName); + const toolInput = + typeof block.input === "object" && block.input !== null + ? (block.input as Record) + : {}; + const itemId = block.id; + const detail = summarizeToolRequest(toolName, toolInput); + + const tool: ToolInFlight = { + itemId, + itemType, + toolName, + title: titleForTool(itemType), + detail, + }; + context.inFlightTools.set(index, tool); + + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "item.started", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), + itemId: asRuntimeItemId(tool.itemId), + payload: { + itemType: tool.itemType, + status: "inProgress", + title: tool.title, + ...(tool.detail ? { detail: tool.detail } : {}), + data: { + toolName: tool.toolName, + input: toolInput, + }, + }, + providerRefs: { + ...providerThreadRef(context), + ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), + providerItemId: ProviderItemId.makeUnsafe(tool.itemId), + }, + raw: { + source: "claude.sdk.message", + method: "claude/stream_event/content_block_start", + payload: message, + }, + }); + return; + } + + if (event.type === "content_block_stop") { + const { index } = event; + const tool = context.inFlightTools.get(index); + if (!tool) { + return; + } + context.inFlightTools.delete(index); + + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "item.completed", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), + itemId: asRuntimeItemId(tool.itemId), + payload: { + itemType: tool.itemType, + status: "completed", + title: tool.title, + ...(tool.detail ? { detail: tool.detail } : {}), + }, + providerRefs: { + ...providerThreadRef(context), + ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), + providerItemId: ProviderItemId.makeUnsafe(tool.itemId), + }, + raw: { + source: "claude.sdk.message", + method: "claude/stream_event/content_block_stop", + payload: message, + }, + }); + } + }); + + const handleAssistantMessage = ( + context: ClaudeSessionContext, + message: SDKMessage, + ): Effect.Effect => + Effect.gen(function* () { + if (message.type !== "assistant") { + return; + } + + if (context.turnState) { + context.turnState.items.push(message.message); + const fallbackAssistantText = extractAssistantText(message); + if ( + fallbackAssistantText.length > 0 && + fallbackAssistantText !== context.turnState.fallbackAssistantText + ) { + context.turnState = { + ...context.turnState, + fallbackAssistantText, + }; + } + + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "item.updated", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + turnId: context.turnState.turnId, + itemId: asRuntimeItemId(context.turnState.assistantItemId), + payload: { + itemType: "assistant_message", + status: "inProgress", + title: "Assistant message", + data: message.message, + }, + providerRefs: { + ...providerThreadRef(context), + providerTurnId: context.turnState.turnId, + providerItemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId), + }, + raw: { + source: "claude.sdk.message", + method: "claude/assistant", + payload: message, + }, + }); + } + + context.lastAssistantUuid = message.uuid; + yield* updateResumeCursor(context); + }); + + const handleResultMessage = ( + context: ClaudeSessionContext, + message: SDKMessage, + ): Effect.Effect => + Effect.gen(function* () { + if (message.type !== "result") { + return; + } + + const status = turnStatusFromResult(message); + const errorMessage = message.subtype === "success" ? undefined : message.errors[0]; + + if (status === "failed") { + yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed."); + } + + yield* completeTurn(context, status, errorMessage, message); + }); + + const handleSystemMessage = ( + context: ClaudeSessionContext, + message: SDKMessage, + ): Effect.Effect => + Effect.gen(function* () { + if (message.type !== "system") { + return; + } + + const stamp = yield* makeEventStamp(); + const base = { + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), + providerRefs: { + ...providerThreadRef(context), + ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), + }, + raw: { + source: "claude.sdk.message" as const, + method: sdkNativeMethod(message), + messageType: `${message.type}:${message.subtype}`, + payload: message, + }, + }; + + switch (message.subtype) { + case "init": + yield* offerRuntimeEvent({ + ...base, + type: "session.configured", + payload: { + config: message as Record, + }, + }); + return; + case "status": + yield* offerRuntimeEvent({ + ...base, + type: "session.state.changed", + payload: { + state: message.status === "compacting" ? "waiting" : "running", + reason: `status:${message.status ?? "active"}`, + detail: message, + }, + }); + return; + case "compact_boundary": + yield* offerRuntimeEvent({ + ...base, + type: "thread.state.changed", + payload: { + state: "compacted", + detail: message, + }, + }); + return; + case "hook_started": + yield* offerRuntimeEvent({ + ...base, + type: "hook.started", + payload: { + hookId: message.hook_id, + hookName: message.hook_name, + hookEvent: message.hook_event, + }, + }); + return; + case "hook_progress": + yield* offerRuntimeEvent({ + ...base, + type: "hook.progress", + payload: { + hookId: message.hook_id, + output: message.output, + stdout: message.stdout, + stderr: message.stderr, + }, + }); + return; + case "hook_response": + yield* offerRuntimeEvent({ + ...base, + type: "hook.completed", + payload: { + hookId: message.hook_id, + outcome: message.outcome, + output: message.output, + stdout: message.stdout, + stderr: message.stderr, + ...(typeof message.exit_code === "number" ? { exitCode: message.exit_code } : {}), + }, + }); + return; + case "task_started": + yield* offerRuntimeEvent({ + ...base, + type: "task.started", + payload: { + taskId: RuntimeTaskId.makeUnsafe(message.task_id), + description: message.description, + ...(message.task_type ? { taskType: message.task_type } : {}), + }, + }); + return; + case "task_progress": + yield* offerRuntimeEvent({ + ...base, + type: "task.progress", + payload: { + taskId: RuntimeTaskId.makeUnsafe(message.task_id), + description: message.description, + ...(message.usage ? { usage: message.usage } : {}), + ...(message.last_tool_name ? { lastToolName: message.last_tool_name } : {}), + }, + }); + return; + case "task_notification": + yield* offerRuntimeEvent({ + ...base, + type: "task.completed", + payload: { + taskId: RuntimeTaskId.makeUnsafe(message.task_id), + status: message.status, + ...(message.summary ? { summary: message.summary } : {}), + ...(message.usage ? { usage: message.usage } : {}), + }, + }); + return; + case "files_persisted": + yield* offerRuntimeEvent({ + ...base, + type: "files.persisted", + payload: { + files: Array.isArray(message.files) + ? message.files.map((file: { filename: string; file_id: string }) => ({ + filename: file.filename, + fileId: file.file_id, + })) + : [], + ...(Array.isArray(message.failed) + ? { + failed: message.failed.map((entry: { filename: string; error: string }) => ({ + filename: entry.filename, + error: entry.error, + })), + } + : {}), + }, + }); + return; + default: + yield* emitRuntimeWarning( + context, + `Unhandled Claude system message subtype '${message.subtype}'.`, + message, + ); + return; + } + }); + + const handleSdkTelemetryMessage = ( + context: ClaudeSessionContext, + message: SDKMessage, + ): Effect.Effect => + Effect.gen(function* () { + const stamp = yield* makeEventStamp(); + const base = { + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), + providerRefs: { + ...providerThreadRef(context), + ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), + }, + raw: { + source: "claude.sdk.message" as const, + method: sdkNativeMethod(message), + messageType: message.type, + payload: message, + }, + }; + + if (message.type === "tool_progress") { + yield* offerRuntimeEvent({ + ...base, + type: "tool.progress", + payload: { + toolUseId: message.tool_use_id, + toolName: message.tool_name, + elapsedSeconds: message.elapsed_time_seconds, + ...(message.task_id ? { summary: `task:${message.task_id}` } : {}), + }, + }); + return; + } + + if (message.type === "tool_use_summary") { + yield* offerRuntimeEvent({ + ...base, + type: "tool.summary", + payload: { + summary: message.summary, + ...(message.preceding_tool_use_ids.length > 0 + ? { precedingToolUseIds: message.preceding_tool_use_ids } + : {}), + }, + }); + return; + } + + if (message.type === "auth_status") { + yield* offerRuntimeEvent({ + ...base, + type: "auth.status", + payload: { + isAuthenticating: message.isAuthenticating, + output: message.output, + ...(message.error ? { error: message.error } : {}), + }, + }); + return; + } + + if (message.type === "rate_limit_event") { + yield* offerRuntimeEvent({ + ...base, + type: "account.rate-limits.updated", + payload: { + rateLimits: message, + }, + }); + return; + } + }); + + const handleSdkMessage = ( + context: ClaudeSessionContext, + message: SDKMessage, + ): Effect.Effect => + Effect.gen(function* () { + yield* logNativeSdkMessage(context, message); + yield* ensureThreadId(context, message); + + switch (message.type) { + case "stream_event": + yield* handleStreamEvent(context, message); + return; + case "user": + return; + case "assistant": + yield* handleAssistantMessage(context, message); + return; + case "result": + yield* handleResultMessage(context, message); + return; + case "system": + yield* handleSystemMessage(context, message); + return; + case "tool_progress": + case "tool_use_summary": + case "auth_status": + case "rate_limit_event": + yield* handleSdkTelemetryMessage(context, message); + return; + default: + yield* emitRuntimeWarning( + context, + `Unhandled Claude SDK message type '${message.type}'.`, + message, + ); + return; + } + }); + + const runSdkStream = (context: ClaudeSessionContext): Effect.Effect => + Stream.fromAsyncIterable(context.query, (cause) => cause).pipe( + Stream.takeWhile(() => !context.stopped), + Stream.runForEach((message) => handleSdkMessage(context, message)), + Effect.catchCause((cause) => + Effect.gen(function* () { + if (Cause.hasInterruptsOnly(cause) || context.stopped) { + return; + } + const message = toMessage(Cause.squash(cause), "Claude runtime stream failed."); + yield* emitRuntimeError(context, message, cause); + yield* completeTurn(context, "failed", message); + }), + ), + ); + + const stopSessionInternal = ( + context: ClaudeSessionContext, + options?: { readonly emitExitEvent?: boolean }, + ): Effect.Effect => + Effect.gen(function* () { + if (context.stopped) return; + + context.stopped = true; + + for (const [requestId, pending] of context.pendingApprovals) { + yield* Deferred.succeed(pending.decision, "cancel"); + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "request.resolved", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), + requestId: asRuntimeRequestId(requestId), + payload: { + requestType: pending.requestType, + decision: "cancel", + }, + providerRefs: { + ...providerThreadRef(context), + ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), + providerRequestId: requestId, + }, + }); + } + context.pendingApprovals.clear(); + + if (context.turnState) { + yield* completeTurn(context, "interrupted", "Session stopped."); + } + + yield* Queue.shutdown(context.promptQueue); + + context.query.close(); + + const updatedAt = yield* nowIso; + context.session = { + ...context.session, + status: "closed", + activeTurnId: undefined, + updatedAt, + }; + + if (options?.emitExitEvent !== false) { + const stamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "session.exited", + eventId: stamp.eventId, + provider: PROVIDER, + createdAt: stamp.createdAt, + threadId: context.session.threadId, + payload: { + reason: "Session stopped", + exitKind: "graceful", + }, + providerRefs: {}, + }); + } + + sessions.delete(context.session.threadId); + }); + + const requireSession = ( + threadId: ThreadId, + ): Effect.Effect => { + const context = sessions.get(threadId); + if (!context) { + return Effect.fail( + new ProviderAdapterSessionNotFoundError({ + provider: PROVIDER, + threadId, + }), + ); + } + if (context.stopped || context.session.status === "closed") { + return Effect.fail( + new ProviderAdapterSessionClosedError({ + provider: PROVIDER, + threadId, + }), + ); + } + return Effect.succeed(context); + }; + + const startSession: ClaudeCodeAdapterShape["startSession"] = (input) => + Effect.gen(function* () { + if (input.provider !== undefined && input.provider !== PROVIDER) { + return yield* new ProviderAdapterValidationError({ + provider: PROVIDER, + operation: "startSession", + issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`, + }); + } + + const startedAt = yield* nowIso; + const resumeState = readClaudeResumeState(input.resumeCursor); + const threadId = input.threadId; + + const promptQueue = yield* Queue.unbounded(); + const prompt = Stream.fromQueue(promptQueue).pipe( + Stream.filter((item) => item.type === "message"), + Stream.map((item) => item.message), + Stream.toAsyncIterable, + ); + + const pendingApprovals = new Map(); + const inFlightTools = new Map(); + + const contextRef = yield* Ref.make(undefined); + + const canUseTool: CanUseTool = (toolName, toolInput, callbackOptions) => + Effect.runPromise( + Effect.gen(function* () { + const context = yield* Ref.get(contextRef); + if (!context) { + return { + behavior: "deny", + message: "Claude session context is unavailable.", + } satisfies PermissionResult; + } + + const runtimeMode = input.runtimeMode ?? "full-access"; + if (runtimeMode === "full-access") { + return { + behavior: "allow", + updatedInput: toolInput, + } satisfies PermissionResult; + } + + const requestId = ApprovalRequestId.makeUnsafe(yield* Random.nextUUIDv4); + const requestType = classifyRequestType(toolName); + const detail = summarizeToolRequest(toolName, toolInput); + const decisionDeferred = yield* Deferred.make(); + const pendingApproval: PendingApproval = { + requestType, + detail, + decision: decisionDeferred, + ...(callbackOptions.suggestions + ? { suggestions: callbackOptions.suggestions } + : {}), + }; + + const requestedStamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "request.opened", + eventId: requestedStamp.eventId, + provider: PROVIDER, + createdAt: requestedStamp.createdAt, + threadId: context.session.threadId, + ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), + requestId: asRuntimeRequestId(requestId), + payload: { + requestType, + detail, + args: { + toolName, + input: toolInput, + ...(callbackOptions.toolUseID ? { toolUseId: callbackOptions.toolUseID } : {}), + }, + }, + providerRefs: { + ...(context.session.threadId + ? { providerThreadId: context.session.threadId } + : {}), + ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), + providerRequestId: requestId, + }, + raw: { + source: "claude.sdk.permission", + method: "canUseTool/request", + payload: { + toolName, + input: toolInput, + }, + }, + }); + + pendingApprovals.set(requestId, pendingApproval); + + const onAbort = () => { + if (!pendingApprovals.has(requestId)) { + return; + } + pendingApprovals.delete(requestId); + Effect.runFork(Deferred.succeed(decisionDeferred, "cancel")); + }; + + callbackOptions.signal.addEventListener("abort", onAbort, { + once: true, + }); + + const decision = yield* Deferred.await(decisionDeferred); + pendingApprovals.delete(requestId); + + const resolvedStamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "request.resolved", + eventId: resolvedStamp.eventId, + provider: PROVIDER, + createdAt: resolvedStamp.createdAt, + threadId: context.session.threadId, + ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), + requestId: asRuntimeRequestId(requestId), + payload: { + requestType, + decision, + }, + providerRefs: { + ...(context.session.threadId + ? { providerThreadId: context.session.threadId } + : {}), + ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), + providerRequestId: requestId, + }, + raw: { + source: "claude.sdk.permission", + method: "canUseTool/decision", + payload: { + decision, + }, + }, + }); + + if (decision === "accept" || decision === "acceptForSession") { + return { + behavior: "allow", + updatedInput: toolInput, + ...(decision === "acceptForSession" && pendingApproval.suggestions + ? { updatedPermissions: [...pendingApproval.suggestions] } + : {}), + } satisfies PermissionResult; + } + + return { + behavior: "deny", + message: + decision === "cancel" + ? "User cancelled tool execution." + : "User declined tool execution.", + } satisfies PermissionResult; + }), + ); + + const providerOptions = input.providerOptions?.claudeCode; + const permissionMode = + toPermissionMode(providerOptions?.permissionMode) ?? + (input.runtimeMode === "full-access" ? "bypassPermissions" : undefined); + + const queryOptions: ClaudeQueryOptions = { + ...(input.cwd ? { cwd: input.cwd } : {}), + ...(input.model ? { model: input.model } : {}), + ...(providerOptions?.binaryPath + ? { pathToClaudeCodeExecutable: providerOptions.binaryPath } + : {}), + ...(permissionMode ? { permissionMode } : {}), + ...(permissionMode === "bypassPermissions" + ? { allowDangerouslySkipPermissions: true } + : {}), + ...(providerOptions?.maxThinkingTokens !== undefined + ? { maxThinkingTokens: providerOptions.maxThinkingTokens } + : {}), + ...(resumeState?.resume ? { resume: resumeState.resume } : {}), + ...(resumeState?.resumeSessionAt ? { resumeSessionAt: resumeState.resumeSessionAt } : {}), + includePartialMessages: true, + canUseTool, + env: process.env, + ...(input.cwd ? { additionalDirectories: [input.cwd] } : {}), + }; + + const queryRuntime = yield* Effect.try({ + try: () => + createQuery({ + prompt, + options: queryOptions, + }), + catch: (cause) => + new ProviderAdapterProcessError({ + provider: PROVIDER, + threadId, + detail: toMessage(cause, "Failed to start Claude runtime session."), + cause, + }), + }); + + const session: ProviderSession = { + threadId, + provider: PROVIDER, + status: "ready", + runtimeMode: input.runtimeMode, + ...(input.cwd ? { cwd: input.cwd } : {}), + ...(input.model ? { model: input.model } : {}), + ...(threadId ? { threadId } : {}), + resumeCursor: { + ...(threadId ? { threadId } : {}), + ...(resumeState?.resume ? { resume: resumeState.resume } : {}), + ...(resumeState?.resumeSessionAt + ? { resumeSessionAt: resumeState.resumeSessionAt } + : {}), + turnCount: resumeState?.turnCount ?? 0, + }, + createdAt: startedAt, + updatedAt: startedAt, + }; + + const context: ClaudeSessionContext = { + session, + promptQueue, + query: queryRuntime, + startedAt, + resumeSessionId: resumeState?.resume, + pendingApprovals, + turns: [], + inFlightTools, + turnState: undefined, + lastAssistantUuid: resumeState?.resumeSessionAt, + lastThreadStartedId: undefined, + stopped: false, + }; + yield* Ref.set(contextRef, context); + sessions.set(threadId, context); + + const sessionStartedStamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "session.started", + eventId: sessionStartedStamp.eventId, + provider: PROVIDER, + createdAt: sessionStartedStamp.createdAt, + threadId, + payload: input.resumeCursor !== undefined ? { resume: input.resumeCursor } : {}, + providerRefs: {}, + }); + + const configuredStamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "session.configured", + eventId: configuredStamp.eventId, + provider: PROVIDER, + createdAt: configuredStamp.createdAt, + threadId, + payload: { + config: { + ...(input.model ? { model: input.model } : {}), + ...(input.cwd ? { cwd: input.cwd } : {}), + ...(permissionMode ? { permissionMode } : {}), + ...(providerOptions?.maxThinkingTokens !== undefined + ? { maxThinkingTokens: providerOptions.maxThinkingTokens } + : {}), + }, + }, + providerRefs: {}, + }); + + const readyStamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "session.state.changed", + eventId: readyStamp.eventId, + provider: PROVIDER, + createdAt: readyStamp.createdAt, + threadId, + payload: { + state: "ready", + }, + providerRefs: {}, + }); + + Effect.runFork(runSdkStream(context)); + + return { + ...session, + }; + }); + + const sendTurn: ClaudeCodeAdapterShape["sendTurn"] = (input) => + Effect.gen(function* () { + const context = yield* requireSession(input.threadId); + + if (context.turnState) { + return yield* new ProviderAdapterValidationError({ + provider: PROVIDER, + operation: "sendTurn", + issue: `Thread '${input.threadId}' already has an active turn '${context.turnState.turnId}'.`, + }); + } + + if (input.model) { + yield* Effect.tryPromise({ + try: () => context.query.setModel(input.model), + catch: (cause) => toRequestError(input.threadId, "turn/setModel", cause), + }); + } + + const turnId = TurnId.makeUnsafe(yield* Random.nextUUIDv4); + const turnState: ClaudeTurnState = { + turnId, + assistantItemId: yield* Random.nextUUIDv4, + startedAt: yield* nowIso, + items: [], + messageCompleted: false, + emittedTextDelta: false, + fallbackAssistantText: "", + }; + + const updatedAt = yield* nowIso; + context.turnState = turnState; + context.session = { + ...context.session, + status: "running", + activeTurnId: turnId, + updatedAt, + }; + + const turnStartedStamp = yield* makeEventStamp(); + yield* offerRuntimeEvent({ + type: "turn.started", + eventId: turnStartedStamp.eventId, + provider: PROVIDER, + createdAt: turnStartedStamp.createdAt, + threadId: context.session.threadId, + turnId, + payload: input.model ? { model: input.model } : {}, + providerRefs: { + providerTurnId: String(turnId), + }, + }); + + const message = buildUserMessage(input); + + yield* Queue.offer(context.promptQueue, { + type: "message", + message, + }).pipe(Effect.mapError((cause) => toRequestError(input.threadId, "turn/start", cause))); + + return { + threadId: context.session.threadId, + turnId, + ...(context.session.resumeCursor !== undefined + ? { resumeCursor: context.session.resumeCursor } + : {}), + }; + }); + + const interruptTurn: ClaudeCodeAdapterShape["interruptTurn"] = (threadId, _turnId) => + Effect.gen(function* () { + const context = yield* requireSession(threadId); + yield* Effect.tryPromise({ + try: () => context.query.interrupt(), + catch: (cause) => toRequestError(threadId, "turn/interrupt", cause), + }); + }); + + const readThread: ClaudeCodeAdapterShape["readThread"] = (threadId) => + Effect.gen(function* () { + const context = yield* requireSession(threadId); + return yield* snapshotThread(context); + }); + + const rollbackThread: ClaudeCodeAdapterShape["rollbackThread"] = (threadId, numTurns) => + Effect.gen(function* () { + const context = yield* requireSession(threadId); + const nextLength = Math.max(0, context.turns.length - numTurns); + context.turns.splice(nextLength); + yield* updateResumeCursor(context); + return yield* snapshotThread(context); + }); + + const respondToRequest: ClaudeCodeAdapterShape["respondToRequest"] = ( + threadId, + requestId, + decision, + ) => + Effect.gen(function* () { + const context = yield* requireSession(threadId); + const pending = context.pendingApprovals.get(requestId); + if (!pending) { + return yield* new ProviderAdapterRequestError({ + provider: PROVIDER, + method: "item/requestApproval/decision", + detail: `Unknown pending approval request: ${requestId}`, + }); + } + + context.pendingApprovals.delete(requestId); + yield* Deferred.succeed(pending.decision, decision); + }); + + const respondToUserInput: ClaudeCodeAdapterShape["respondToUserInput"] = ( + threadId, + requestId, + _answers, + ) => + Effect.fail( + new ProviderAdapterRequestError({ + provider: PROVIDER, + method: "item/tool/requestUserInput", + detail: `Claude Code does not yet support structured user-input responses for thread '${threadId}' and request '${requestId}'.`, + }), + ); + + const stopSession: ClaudeCodeAdapterShape["stopSession"] = (threadId) => + Effect.gen(function* () { + const context = yield* requireSession(threadId); + yield* stopSessionInternal(context, { + emitExitEvent: true, + }); + }); + + const listSessions: ClaudeCodeAdapterShape["listSessions"] = () => + Effect.sync(() => Array.from(sessions.values(), ({ session }) => ({ ...session }))); + + const hasSession: ClaudeCodeAdapterShape["hasSession"] = (threadId) => + Effect.sync(() => { + const context = sessions.get(threadId); + return context !== undefined && !context.stopped; + }); + + const stopAll: ClaudeCodeAdapterShape["stopAll"] = () => + Effect.forEach( + sessions, + ([, context]) => + stopSessionInternal(context, { + emitExitEvent: true, + }), + { discard: true }, + ); + + yield* Effect.addFinalizer(() => + Effect.forEach( + sessions, + ([, context]) => + stopSessionInternal(context, { + emitExitEvent: false, + }), + { discard: true }, + ).pipe(Effect.tap(() => Queue.shutdown(runtimeEventQueue))), + ); + + return { + provider: PROVIDER, + capabilities: { + sessionModelSwitch: "in-session", + }, + startSession, + sendTurn, + interruptTurn, + readThread, + rollbackThread, + respondToRequest, + respondToUserInput, + stopSession, + listSessions, + hasSession, + stopAll, + streamEvents: Stream.fromQueue(runtimeEventQueue), + } satisfies ClaudeCodeAdapterShape; + }); +} + +export const ClaudeCodeAdapterLive = Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter()); + +export function makeClaudeCodeAdapterLive(options?: ClaudeCodeAdapterLiveOptions) { + return Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter(options)); +} diff --git a/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts b/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts index c6f4a3c08..795c106f0 100644 --- a/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts @@ -4,6 +4,7 @@ import { assertFailure } from "@effect/vitest/utils"; import { Effect, Layer, Stream } from "effect"; +import { ClaudeCodeAdapter, ClaudeCodeAdapterShape } from "../Services/ClaudeCodeAdapter.ts"; import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts"; import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts"; import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts"; @@ -27,9 +28,32 @@ const fakeCodexAdapter: CodexAdapterShape = { streamEvents: Stream.empty, }; +const fakeClaudeAdapter: ClaudeCodeAdapterShape = { + provider: "claudeCode", + capabilities: { sessionModelSwitch: "in-session" }, + startSession: vi.fn(), + sendTurn: vi.fn(), + interruptTurn: vi.fn(), + respondToRequest: vi.fn(), + respondToUserInput: vi.fn(), + stopSession: vi.fn(), + listSessions: vi.fn(), + hasSession: vi.fn(), + readThread: vi.fn(), + rollbackThread: vi.fn(), + stopAll: vi.fn(), + streamEvents: Stream.empty, +}; + const layer = it.layer( Layer.mergeAll( - Layer.provide(ProviderAdapterRegistryLive, Layer.succeed(CodexAdapter, fakeCodexAdapter)), + Layer.provide( + ProviderAdapterRegistryLive, + Layer.mergeAll( + Layer.succeed(CodexAdapter, fakeCodexAdapter), + Layer.succeed(ClaudeCodeAdapter, fakeClaudeAdapter), + ), + ), NodeServices.layer, ), ); @@ -39,10 +63,12 @@ layer("ProviderAdapterRegistryLive", (it) => { Effect.gen(function* () { const registry = yield* ProviderAdapterRegistry; const codex = yield* registry.getByProvider("codex"); + const claude = yield* registry.getByProvider("claudeCode"); assert.equal(codex, fakeCodexAdapter); + assert.equal(claude, fakeClaudeAdapter); const providers = yield* registry.listProviders(); - assert.deepEqual(providers, ["codex"]); + assert.deepEqual(providers, ["codex", "claudeCode"]); }), ); diff --git a/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts b/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts index 3062ed790..61fa2d18c 100644 --- a/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts +++ b/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts @@ -15,6 +15,7 @@ import { ProviderAdapterRegistry, type ProviderAdapterRegistryShape, } from "../Services/ProviderAdapterRegistry.ts"; +import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts"; import { CodexAdapter } from "../Services/CodexAdapter.ts"; export interface ProviderAdapterRegistryLiveOptions { @@ -23,7 +24,10 @@ export interface ProviderAdapterRegistryLiveOptions { const makeProviderAdapterRegistry = (options?: ProviderAdapterRegistryLiveOptions) => Effect.gen(function* () { - const adapters = options?.adapters !== undefined ? options.adapters : [yield* CodexAdapter]; + const adapters = + options?.adapters !== undefined + ? options.adapters + : [yield* CodexAdapter, yield* ClaudeCodeAdapter]; const byProvider = new Map(adapters.map((adapter) => [adapter.provider, adapter])); const getByProvider: ProviderAdapterRegistryShape["getByProvider"] = (provider) => { diff --git a/apps/server/src/provider/Services/ClaudeCodeAdapter.ts b/apps/server/src/provider/Services/ClaudeCodeAdapter.ts new file mode 100644 index 000000000..80fb8771d --- /dev/null +++ b/apps/server/src/provider/Services/ClaudeCodeAdapter.ts @@ -0,0 +1,32 @@ +/** + * ClaudeCodeAdapter - Claude Code implementation of the generic provider adapter contract. + * + * This service owns Claude runtime/session semantics and emits canonical + * provider runtime events. It does not perform cross-provider routing, shared + * event fan-out, or checkpoint orchestration. + * + * Uses Effect `ServiceMap.Service` for dependency injection and returns the + * shared provider-adapter error channel with `provider: "claudeCode"` context. + * + * @module ClaudeCodeAdapter + */ +import { ServiceMap } from "effect"; + +import type { ProviderAdapterError } from "../Errors.ts"; +import type { ProviderAdapterShape } from "./ProviderAdapter.ts"; + +/** + * ClaudeCodeAdapterShape - Service API for the Claude Code provider adapter. + */ +export interface ClaudeCodeAdapterShape extends ProviderAdapterShape { + readonly provider: "claudeCode"; +} + +/** + * ClaudeCodeAdapter - Service tag for Claude Code provider adapter operations. + */ +export class ClaudeCodeAdapter extends ServiceMap.Service< + ClaudeCodeAdapter, + ClaudeCodeAdapterShape +>()("t3/provider/Services/ClaudeCodeAdapter") {} + diff --git a/apps/server/src/serverLayers.ts b/apps/server/src/serverLayers.ts index ff9b10d96..dea77b9fc 100644 --- a/apps/server/src/serverLayers.ts +++ b/apps/server/src/serverLayers.ts @@ -19,6 +19,7 @@ import { OrchestrationProjectionSnapshotQueryLive } from "./orchestration/Layers import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion"; import { RuntimeReceiptBusLive } from "./orchestration/Layers/RuntimeReceiptBus"; import { ProviderUnsupportedError } from "./provider/Errors"; +import { makeClaudeCodeAdapterLive } from "./provider/Layers/ClaudeCodeAdapter"; import { makeCodexAdapterLive } from "./provider/Layers/CodexAdapter"; import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry"; import { makeProviderServiceLive } from "./provider/Layers/ProviderService"; @@ -58,8 +59,12 @@ export function makeServerProviderLayer(): Layer.Layer< const codexAdapterLayer = makeCodexAdapterLive( nativeEventLogger ? { nativeEventLogger } : undefined, ); + const claudeAdapterLayer = makeClaudeCodeAdapterLive( + nativeEventLogger ? { nativeEventLogger } : undefined, + ); const adapterRegistryLayer = ProviderAdapterRegistryLive.pipe( Layer.provide(codexAdapterLayer), + Layer.provide(claudeAdapterLayer), Layer.provideMerge(providerSessionDirectoryLayer), ); return makeProviderServiceLive( diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index b4afcdefa..3f18e4895 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -54,6 +54,13 @@ const MODEL_PROVIDER_SETTINGS: Array<{ placeholder: "your-codex-model-slug", example: "gpt-6.7-codex-ultra-preview", }, + { + provider: "claudeCode", + title: "Claude Code", + description: "Save additional Claude model slugs for the picker and `/model` command.", + placeholder: "your-claude-model-slug", + example: "claude-sonnet-5-0", + }, ] as const; const TIMESTAMP_FORMAT_LABELS = { diff --git a/apps/web/src/session-logic.test.ts b/apps/web/src/session-logic.test.ts index 74ba3a814..21676828e 100644 --- a/apps/web/src/session-logic.test.ts +++ b/apps/web/src/session-logic.test.ts @@ -673,18 +673,18 @@ describe("deriveActiveWorkStartedAt", () => { }); describe("PROVIDER_OPTIONS", () => { - it("keeps Claude Code and Cursor visible as unavailable placeholders in the stack base", () => { + it("advertises Claude Code on the Claude stack while keeping Cursor as a placeholder", () => { const claude = PROVIDER_OPTIONS.find((option) => option.value === "claudeCode"); const cursor = PROVIDER_OPTIONS.find((option) => option.value === "cursor"); expect(PROVIDER_OPTIONS).toEqual([ { value: "codex", label: "Codex", available: true }, - { value: "claudeCode", label: "Claude Code", available: false }, + { value: "claudeCode", label: "Claude Code", available: true }, { value: "cursor", label: "Cursor", available: false }, ]); expect(claude).toEqual({ value: "claudeCode", label: "Claude Code", - available: false, + available: true, }); expect(cursor).toEqual({ value: "cursor", diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts index e389f10e2..4e75c09dd 100644 --- a/apps/web/src/session-logic.ts +++ b/apps/web/src/session-logic.ts @@ -26,7 +26,7 @@ export const PROVIDER_OPTIONS: Array<{ available: boolean; }> = [ { value: "codex", label: "Codex", available: true }, - { value: "claudeCode", label: "Claude Code", available: false }, + { value: "claudeCode", label: "Claude Code", available: true }, { value: "cursor", label: "Cursor", available: false }, ]; From da9cbcc98c0c4ddde41523b3db62f90145587387 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Mar 2026 20:58:11 -0800 Subject: [PATCH 02/23] test: restore Claude coverage on sibling stack Co-authored-by: codex --- .../Layers/CheckpointReactor.test.ts | 133 ++++++++++++++++- .../Layers/ProviderCommandReactor.test.ts | 137 +++++++++++++++++- .../Layers/ProviderRuntimeIngestion.test.ts | 54 +++++++ .../provider/Layers/ProviderService.test.ts | 29 +++- 4 files changed, 350 insertions(+), 3 deletions(-) diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts index 09773b71d..346ca7b33 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts @@ -235,6 +235,7 @@ describe("CheckpointReactor", () => { readonly projectWorkspaceRoot?: string; readonly threadWorktreePath?: string | null; readonly providerSessionCwd?: string; + readonly providerName?: "codex" | "claudeCode"; }) { const cwd = createGitRepository(); tempDirs.push(cwd); @@ -242,7 +243,7 @@ describe("CheckpointReactor", () => { cwd, options?.hasSession ?? true, options?.providerSessionCwd ?? cwd, - "codex", + options?.providerName ?? "codex", ); const orchestrationLayer = OrchestrationEngineLive.pipe( Layer.provide(OrchestrationProjectionPipelineLive), @@ -477,6 +478,67 @@ describe("CheckpointReactor", () => { expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); }); + it("captures pre-turn and completion checkpoints for claudeCode runtime events", async () => { + const harness = await createHarness({ + seedFilesystemCheckpoints: false, + providerName: "claudeCode", + }); + const createdAt = new Date().toISOString(); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.session.set", + commandId: CommandId.makeUnsafe("cmd-session-set-capture-claude"), + threadId: ThreadId.makeUnsafe("thread-1"), + session: { + threadId: ThreadId.makeUnsafe("thread-1"), + status: "ready", + providerName: "claudeCode", + runtimeMode: "approval-required", + activeTurnId: null, + lastError: null, + updatedAt: createdAt, + }, + createdAt, + }), + ); + + harness.provider.emit({ + type: "turn.started", + eventId: EventId.makeUnsafe("evt-turn-started-claude-1"), + provider: "claudeCode", + createdAt: new Date().toISOString(), + threadId: ThreadId.makeUnsafe("thread-1"), + turnId: asTurnId("turn-claude-1"), + }); + await waitForGitRefExists( + harness.cwd, + checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 0), + ); + + fs.writeFileSync(path.join(harness.cwd, "README.md"), "v2\n", "utf8"); + harness.provider.emit({ + type: "turn.completed", + eventId: EventId.makeUnsafe("evt-turn-completed-claude-1"), + provider: "claudeCode", + createdAt: new Date().toISOString(), + threadId: ThreadId.makeUnsafe("thread-1"), + turnId: asTurnId("turn-claude-1"), + payload: { state: "completed" }, + }); + + await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed"); + const thread = await waitForThread( + harness.engine, + (entry) => entry.latestTurn?.turnId === "turn-claude-1" && entry.checkpoints.length === 1, + ); + + expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); + expect( + gitRefExists(harness.cwd, checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 1)), + ).toBe(true); + }); + it("appends capture failure activity when turn diff summary cannot be derived", async () => { const harness = await createHarness({ seedFilesystemCheckpoints: false }); const createdAt = new Date().toISOString(); @@ -792,6 +854,75 @@ describe("CheckpointReactor", () => { ).toBe(false); }); + it("executes provider revert and emits thread.reverted for claudeCode sessions", async () => { + const harness = await createHarness({ providerName: "claudeCode" }); + const createdAt = new Date().toISOString(); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.session.set", + commandId: CommandId.makeUnsafe("cmd-session-set-claude"), + threadId: ThreadId.makeUnsafe("thread-1"), + session: { + threadId: ThreadId.makeUnsafe("thread-1"), + status: "ready", + providerName: "claudeCode", + runtimeMode: "approval-required", + activeTurnId: null, + lastError: null, + updatedAt: createdAt, + }, + createdAt, + }), + ); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.diff.complete", + commandId: CommandId.makeUnsafe("cmd-diff-claude-1"), + threadId: ThreadId.makeUnsafe("thread-1"), + turnId: asTurnId("turn-claude-1"), + completedAt: createdAt, + checkpointRef: checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 1), + status: "ready", + files: [], + checkpointTurnCount: 1, + createdAt, + }), + ); + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.diff.complete", + commandId: CommandId.makeUnsafe("cmd-diff-claude-2"), + threadId: ThreadId.makeUnsafe("thread-1"), + turnId: asTurnId("turn-claude-2"), + completedAt: createdAt, + checkpointRef: checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 2), + status: "ready", + files: [], + checkpointTurnCount: 2, + createdAt, + }), + ); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.checkpoint.revert", + commandId: CommandId.makeUnsafe("cmd-revert-request-claude"), + threadId: ThreadId.makeUnsafe("thread-1"), + turnCount: 1, + createdAt, + }), + ); + + await waitForEvent(harness.engine, (event) => event.type === "thread.reverted"); + expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(1); + expect(harness.provider.rollbackConversation).toHaveBeenCalledWith({ + threadId: ThreadId.makeUnsafe("thread-1"), + numTurns: 1, + }); + }); + it("processes consecutive revert requests with deterministic rollback sequencing", async () => { const harness = await createHarness(); const createdAt = new Date().toISOString(); diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts index 8de44d78f..af37cc96a 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts @@ -96,7 +96,9 @@ describe("ProviderCommandReactor", () => { typeof input === "object" && input !== null && "provider" in input && - input.provider === "codex" + (input.provider === "codex" || + input.provider === "claudeCode" || + input.provider === "cursor") ? input.provider : "codex"; const resumeCursor = @@ -389,6 +391,79 @@ describe("ProviderCommandReactor", () => { }); }); + it("starts first turn with requested provider when provider is specified", async () => { + const harness = await createHarness(); + const now = new Date().toISOString(); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-provider-first"), + threadId: ThreadId.makeUnsafe("thread-1"), + message: { + messageId: asMessageId("user-message-provider-first"), + role: "user", + text: "hello claude", + attachments: [], + }, + provider: "claudeCode", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }), + ); + + await waitFor(() => harness.startSession.mock.calls.length === 1); + await waitFor(() => harness.sendTurn.mock.calls.length === 1); + expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({ + provider: "claudeCode", + cwd: "/tmp/provider-project", + model: "gpt-5-codex", + runtimeMode: "approval-required", + }); + + const readModel = await Effect.runPromise(harness.engine.getReadModel()); + const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); + expect(thread?.session?.providerName).toBe("claudeCode"); + expect(thread?.session?.threadId).toBe("thread-1"); + }); + + it("starts first turn with cursor provider when provider is specified", async () => { + const harness = await createHarness(); + const now = new Date().toISOString(); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-provider-cursor"), + threadId: ThreadId.makeUnsafe("thread-1"), + message: { + messageId: asMessageId("user-message-provider-cursor"), + role: "user", + text: "hello cursor", + attachments: [], + }, + provider: "cursor", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }), + ); + + await waitFor(() => harness.startSession.mock.calls.length === 1); + await waitFor(() => harness.sendTurn.mock.calls.length === 1); + expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({ + provider: "cursor", + cwd: "/tmp/provider-project", + model: "gpt-5-codex", + runtimeMode: "approval-required", + }); + + const readModel = await Effect.runPromise(harness.engine.getReadModel()); + const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); + expect(thread?.session?.providerName).toBe("cursor"); + expect(thread?.session?.threadId).toBe("thread-1"); + }); it("reuses the same provider session when runtime mode is unchanged", async () => { const harness = await createHarness(); const now = new Date().toISOString(); @@ -522,6 +597,66 @@ describe("ProviderCommandReactor", () => { expect(thread?.session?.runtimeMode).toBe("approval-required"); }); + it("switches provider by restarting the session when turn request provider changes", async () => { + const harness = await createHarness(); + const now = new Date().toISOString(); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-provider-switch-1"), + threadId: ThreadId.makeUnsafe("thread-1"), + message: { + messageId: asMessageId("user-message-provider-switch-1"), + role: "user", + text: "first", + attachments: [], + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }), + ); + + await waitFor(() => harness.startSession.mock.calls.length === 1); + await waitFor(() => harness.sendTurn.mock.calls.length === 1); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-provider-switch-2"), + threadId: ThreadId.makeUnsafe("thread-1"), + message: { + messageId: asMessageId("user-message-provider-switch-2"), + role: "user", + text: "second", + attachments: [], + }, + provider: "claudeCode", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }), + ); + + await waitFor(() => harness.startSession.mock.calls.length === 2); + await waitFor(() => harness.sendTurn.mock.calls.length === 2); + + expect(harness.stopSession.mock.calls.length).toBe(0); + expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({ + threadId: ThreadId.makeUnsafe("thread-1"), + provider: "claudeCode", + runtimeMode: "approval-required", + }); + expect(harness.startSession.mock.calls[1]?.[1]).not.toHaveProperty("resumeCursor"); + + const readModel = await Effect.runPromise(harness.engine.getReadModel()); + const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); + expect(thread?.session?.threadId).toBe("thread-1"); + expect(thread?.session?.providerName).toBe("claudeCode"); + expect(thread?.session?.runtimeMode).toBe("approval-required"); + }); + it("does not stop the active session when restart fails before rebind", async () => { const harness = await createHarness(); const now = new Date().toISOString(); diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts index b6b48c7ed..525cd0214 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts @@ -402,6 +402,60 @@ describe("ProviderRuntimeIngestion", () => { ); }); + it("accepts claude turn lifecycle when seeded thread id is a synthetic placeholder", async () => { + const harness = await createHarness(); + const seededAt = new Date().toISOString(); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.session.set", + commandId: CommandId.makeUnsafe("cmd-session-seed-claude-placeholder"), + threadId: ThreadId.makeUnsafe("thread-1"), + session: { + threadId: ThreadId.makeUnsafe("thread-1"), + status: "ready", + providerName: "claudeCode", + runtimeMode: "approval-required", + activeTurnId: null, + updatedAt: seededAt, + lastError: null, + }, + createdAt: seededAt, + }), + ); + + harness.emit({ + type: "turn.started", + eventId: asEventId("evt-turn-started-claude-placeholder"), + provider: "claudeCode", + createdAt: new Date().toISOString(), + threadId: asThreadId("thread-1"), + turnId: asTurnId("turn-claude-placeholder"), + }); + + await waitForThread( + harness.engine, + (thread) => + thread.session?.status === "running" && + thread.session?.activeTurnId === "turn-claude-placeholder", + ); + + harness.emit({ + type: "turn.completed", + eventId: asEventId("evt-turn-completed-claude-placeholder"), + provider: "claudeCode", + createdAt: new Date().toISOString(), + threadId: asThreadId("thread-1"), + turnId: asTurnId("turn-claude-placeholder"), + status: "completed", + }); + + await waitForThread( + harness.engine, + (thread) => thread.session?.status === "ready" && thread.session?.activeTurnId === null, + ); + }); + it("ignores auxiliary turn completions from a different provider thread", async () => { const harness = await createHarness(); const now = new Date().toISOString(); diff --git a/apps/server/src/provider/Layers/ProviderService.test.ts b/apps/server/src/provider/Layers/ProviderService.test.ts index d5cf4424b..828dd5b0f 100644 --- a/apps/server/src/provider/Layers/ProviderService.test.ts +++ b/apps/server/src/provider/Layers/ProviderService.test.ts @@ -217,12 +217,15 @@ const sleep = (ms: number) => function makeProviderServiceLayer() { const codex = makeFakeCodexAdapter(); + const claude = makeFakeCodexAdapter("claudeCode"); const registry: typeof ProviderAdapterRegistry.Service = { getByProvider: (provider) => provider === "codex" ? Effect.succeed(codex.adapter) + : provider === "claudeCode" + ? Effect.succeed(claude.adapter) : Effect.fail(new ProviderUnsupportedError({ provider })), - listProviders: () => Effect.succeed(["codex"]), + listProviders: () => Effect.succeed(["codex", "claudeCode"]), }; const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); @@ -247,6 +250,7 @@ function makeProviderServiceLayer() { return { codex, + claude, layer, }; } @@ -533,6 +537,29 @@ routing.layer("ProviderServiceLive routing", (it) => { }), ); + it.effect("routes explicit claudeCode provider session starts to the claude adapter", () => + Effect.gen(function* () { + const provider = yield* ProviderService; + + const session = yield* provider.startSession(asThreadId("thread-claude"), { + provider: "claudeCode", + threadId: asThreadId("thread-claude"), + cwd: "/tmp/project-claude", + runtimeMode: "full-access", + }); + + assert.equal(session.provider, "claudeCode"); + assert.equal(routing.claude.startSession.mock.calls.length, 1); + const startInput = routing.claude.startSession.mock.calls[0]?.[0]; + assert.equal(typeof startInput === "object" && startInput !== null, true); + if (startInput && typeof startInput === "object") { + const startPayload = startInput as { provider?: string; cwd?: string }; + assert.equal(startPayload.provider, "claudeCode"); + assert.equal(startPayload.cwd, "/tmp/project-claude"); + } + }), + ); + it.effect("recovers stale sessions for sendTurn using persisted cwd", () => Effect.gen(function* () { const provider = yield* ProviderService; From 95c50053bcbf30c2454234a87c2e21c449c8ced9 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Mar 2026 21:12:11 -0800 Subject: [PATCH 03/23] contracts: restore Claude schemas on sibling stack Co-authored-by: codex --- packages/contracts/src/model.ts | 17 +++++++++ packages/contracts/src/provider.test.ts | 47 +++++++++++++++++++++++++ packages/contracts/src/provider.ts | 13 +++++++ 3 files changed, 77 insertions(+) diff --git a/packages/contracts/src/model.ts b/packages/contracts/src/model.ts index 189fbf09d..95b86881b 100644 --- a/packages/contracts/src/model.ts +++ b/packages/contracts/src/model.ts @@ -1,6 +1,9 @@ import { Schema } from "effect"; import { ProviderKind } from "./orchestration"; +export const CURSOR_REASONING_OPTIONS = ["low", "normal", "high", "xhigh"] as const; +export type CursorReasoningOption = (typeof CURSOR_REASONING_OPTIONS)[number]; + export const CODEX_REASONING_EFFORT_OPTIONS = ["xhigh", "high", "medium", "low"] as const; export type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORT_OPTIONS)[number]; @@ -10,8 +13,22 @@ export const CodexModelOptions = Schema.Struct({ }); export type CodexModelOptions = typeof CodexModelOptions.Type; +export const ClaudeCodeModelOptions = Schema.Struct({ + thinking: Schema.optional(Schema.Boolean), +}); +export type ClaudeCodeModelOptions = typeof ClaudeCodeModelOptions.Type; + +export const CursorModelOptions = Schema.Struct({ + reasoning: Schema.optional(Schema.Literals(CURSOR_REASONING_OPTIONS)), + fastMode: Schema.optional(Schema.Boolean), + thinking: Schema.optional(Schema.Boolean), +}); +export type CursorModelOptions = typeof CursorModelOptions.Type; + export const ProviderModelOptions = Schema.Struct({ codex: Schema.optional(CodexModelOptions), + claudeCode: Schema.optional(ClaudeCodeModelOptions), + cursor: Schema.optional(CursorModelOptions), }); export type ProviderModelOptions = typeof ProviderModelOptions.Type; diff --git a/packages/contracts/src/provider.test.ts b/packages/contracts/src/provider.test.ts index 997db09b7..97e168a33 100644 --- a/packages/contracts/src/provider.test.ts +++ b/packages/contracts/src/provider.test.ts @@ -42,6 +42,53 @@ describe("ProviderSessionStartInput", () => { }), ).toThrow(); }); + + it("accepts claude runtime knobs", () => { + const parsed = decodeProviderSessionStartInput({ + threadId: "thread-1", + provider: "claudeCode", + cwd: "/tmp/workspace", + model: "claude-sonnet-4-6", + providerOptions: { + claudeCode: { + binaryPath: "/usr/local/bin/claude", + permissionMode: "plan", + maxThinkingTokens: 12_000, + }, + }, + runtimeMode: "full-access", + }); + expect(parsed.provider).toBe("claudeCode"); + expect(parsed.providerOptions?.claudeCode?.binaryPath).toBe("/usr/local/bin/claude"); + expect(parsed.providerOptions?.claudeCode?.permissionMode).toBe("plan"); + expect(parsed.providerOptions?.claudeCode?.maxThinkingTokens).toBe(12_000); + expect(parsed.runtimeMode).toBe("full-access"); + }); + + it("accepts cursor provider payloads", () => { + const parsed = decodeProviderSessionStartInput({ + threadId: "thread-1", + provider: "cursor", + cwd: "/tmp/workspace", + model: "composer-1.5", + modelOptions: { + cursor: { + thinking: true, + }, + }, + providerOptions: { + cursor: { + binaryPath: "/usr/local/bin/agent", + }, + }, + runtimeMode: "approval-required", + }); + expect(parsed.provider).toBe("cursor"); + expect(parsed.model).toBe("composer-1.5"); + expect(parsed.modelOptions?.cursor?.thinking).toBe(true); + expect(parsed.providerOptions?.cursor?.binaryPath).toBe("/usr/local/bin/agent"); + expect(parsed.runtimeMode).toBe("approval-required"); + }); }); describe("ProviderSendTurnInput", () => { diff --git a/packages/contracts/src/provider.ts b/packages/contracts/src/provider.ts index 9d2a198b6..892127220 100644 --- a/packages/contracts/src/provider.ts +++ b/packages/contracts/src/provider.ts @@ -5,6 +5,7 @@ import { ApprovalRequestId, EventId, IsoDateTime, + NonNegativeInt, ProviderItemId, ThreadId, TurnId, @@ -52,8 +53,20 @@ const CodexProviderStartOptions = Schema.Struct({ homePath: Schema.optional(TrimmedNonEmptyStringSchema), }); +const ClaudeCodeProviderStartOptions = Schema.Struct({ + binaryPath: Schema.optional(TrimmedNonEmptyStringSchema), + permissionMode: Schema.optional(TrimmedNonEmptyStringSchema), + maxThinkingTokens: Schema.optional(NonNegativeInt), +}); + +const CursorProviderStartOptions = Schema.Struct({ + binaryPath: Schema.optional(TrimmedNonEmptyStringSchema), +}); + export const ProviderStartOptions = Schema.Struct({ codex: Schema.optional(CodexProviderStartOptions), + claudeCode: Schema.optional(ClaudeCodeProviderStartOptions), + cursor: Schema.optional(CursorProviderStartOptions), }); export type ProviderStartOptions = typeof ProviderStartOptions.Type; From 11aca1fcc6ca7a562ae66b5cb415e4217df4186c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Mar 2026 21:17:26 -0800 Subject: [PATCH 04/23] model: restore Claude catalogs on sibling stack Co-authored-by: codex --- packages/contracts/src/model.ts | 96 ++++++++++++- packages/shared/src/model.test.ts | 136 ++++++++++++++++-- packages/shared/src/model.ts | 220 +++++++++++++++++++++++++++++- 3 files changed, 429 insertions(+), 23 deletions(-) diff --git a/packages/contracts/src/model.ts b/packages/contracts/src/model.ts index 95b86881b..12423767e 100644 --- a/packages/contracts/src/model.ts +++ b/packages/contracts/src/model.ts @@ -1,5 +1,5 @@ import { Schema } from "effect"; -import { ProviderKind } from "./orchestration"; +import type { ProviderKind } from "./orchestration"; export const CURSOR_REASONING_OPTIONS = ["low", "normal", "high", "xhigh"] as const; export type CursorReasoningOption = (typeof CURSOR_REASONING_OPTIONS)[number]; @@ -37,6 +37,25 @@ type ModelOption = { readonly name: string; }; +type CursorModelFamilyOption = { + readonly slug: string; + readonly name: string; +}; + +export const CURSOR_MODEL_FAMILY_OPTIONS = [ + { slug: "auto", name: "Auto" }, + { slug: "composer-1.5", name: "Composer 1.5" }, + { slug: "composer-1", name: "Composer 1" }, + { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, + { slug: "gpt-5.3-codex-spark-preview", name: "GPT-5.3 Codex Spark" }, + { slug: "opus-4.6", name: "Claude 4.6 Opus" }, + { slug: "opus-4.5", name: "Claude 4.5 Opus" }, + { slug: "sonnet-4.6", name: "Claude 4.6 Sonnet" }, + { slug: "gemini-3.1-pro", name: "Gemini 3.1 Pro" }, +] as const satisfies readonly CursorModelFamilyOption[]; + +export type CursorModelFamily = (typeof CURSOR_MODEL_FAMILY_OPTIONS)[number]["slug"]; + export const MODEL_OPTIONS_BY_PROVIDER = { codex: [ { slug: "gpt-5.4", name: "GPT-5.4" }, @@ -45,17 +64,50 @@ export const MODEL_OPTIONS_BY_PROVIDER = { { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" }, { slug: "gpt-5.2", name: "GPT-5.2" }, ], + claudeCode: [ + { slug: "claude-opus-4-6", name: "Claude Opus 4.6" }, + { slug: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" }, + { slug: "claude-haiku-4-5", name: "Claude Haiku 4.5" }, + ], + cursor: [ + { slug: "auto", name: "Auto" }, + { slug: "composer-1.5", name: "Composer 1.5" }, + { slug: "composer-1", name: "Composer 1" }, + { slug: "gpt-5.3-codex-low", name: "GPT-5.3 Codex Low" }, + { slug: "gpt-5.3-codex-low-fast", name: "GPT-5.3 Codex Low Fast" }, + { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, + { slug: "gpt-5.3-codex-fast", name: "GPT-5.3 Codex Fast" }, + { slug: "gpt-5.3-codex-high", name: "GPT-5.3 Codex High" }, + { slug: "gpt-5.3-codex-high-fast", name: "GPT-5.3 Codex High Fast" }, + { slug: "gpt-5.3-codex-xhigh", name: "GPT-5.3 Codex Extra High" }, + { slug: "gpt-5.3-codex-xhigh-fast", name: "GPT-5.3 Codex Extra High Fast" }, + { slug: "gpt-5.3-codex-spark-preview", name: "GPT-5.3 Codex Spark" }, + { slug: "opus-4.6", name: "Claude 4.6 Opus" }, + { slug: "opus-4.6-thinking", name: "Claude 4.6 Opus (Thinking)" }, + { slug: "opus-4.5", name: "Claude 4.5 Opus" }, + { slug: "opus-4.5-thinking", name: "Claude 4.5 Opus (Thinking)" }, + { slug: "sonnet-4.6", name: "Claude 4.6 Sonnet" }, + { slug: "sonnet-4.6-thinking", name: "Claude 4.6 Sonnet (Thinking)" }, + { slug: "gemini-3.1-pro", name: "Gemini 3.1 Pro" }, + ], } as const satisfies Record; export type ModelOptionsByProvider = typeof MODEL_OPTIONS_BY_PROVIDER; -type BuiltInModelSlug = ModelOptionsByProvider[ProviderKind][number]["slug"]; +type BuiltInModelSlug = (typeof MODEL_OPTIONS_BY_PROVIDER)[ProviderKind][number]["slug"]; export type ModelSlug = BuiltInModelSlug | (string & {}); +export type CursorModelSlug = (typeof MODEL_OPTIONS_BY_PROVIDER)["cursor"][number]["slug"]; -export const DEFAULT_MODEL_BY_PROVIDER = { +export const DEFAULT_MODEL_BY_PROVIDER: Record = { codex: "gpt-5.4", -} as const satisfies Record; + claudeCode: "claude-sonnet-4-6", + cursor: "opus-4.6-thinking", +}; + +// Backward compatibility for existing Codex-only call sites. +export const MODEL_OPTIONS = MODEL_OPTIONS_BY_PROVIDER.codex; +export const DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.codex; -export const MODEL_SLUG_ALIASES_BY_PROVIDER = { +export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record> = { codex: { "5.4": "gpt-5.4", "5.3": "gpt-5.3-codex", @@ -63,12 +115,44 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER = { "5.3-spark": "gpt-5.3-codex-spark", "gpt-5.3-spark": "gpt-5.3-codex-spark", }, -} as const satisfies Record>; + claudeCode: { + opus: "claude-opus-4-6", + "opus-4.6": "claude-opus-4-6", + "claude-opus-4.6": "claude-opus-4-6", + "claude-opus-4-6-20251117": "claude-opus-4-6", + sonnet: "claude-sonnet-4-6", + "sonnet-4.6": "claude-sonnet-4-6", + "claude-sonnet-4.6": "claude-sonnet-4-6", + "claude-sonnet-4-6-20251117": "claude-sonnet-4-6", + haiku: "claude-haiku-4-5", + "haiku-4.5": "claude-haiku-4-5", + "claude-haiku-4.5": "claude-haiku-4-5", + "claude-haiku-4-5-20251001": "claude-haiku-4-5", + }, + cursor: { + composer: "composer-1.5", + "composer-1.5": "composer-1.5", + "composer-1": "composer-1", + "gpt-5.3-codex": "gpt-5.3-codex", + "gpt-5.3-codex-spark": "gpt-5.3-codex-spark-preview", + "gemini-3.1-pro": "gemini-3.1-pro", + "claude-4.6-sonnet-thinking": "sonnet-4.6-thinking", + "claude-4.6-opus-thinking": "opus-4.6-thinking", + "claude-4.5-opus-thinking": "opus-4.5-thinking", + "sonnet-4.6-thinking": "sonnet-4.6-thinking", + "opus-4.6-thinking": "opus-4.6-thinking", + "opus-4.5-thinking": "opus-4.5-thinking", + }, +}; export const REASONING_EFFORT_OPTIONS_BY_PROVIDER = { codex: CODEX_REASONING_EFFORT_OPTIONS, + claudeCode: [], + cursor: [], } as const satisfies Record; export const DEFAULT_REASONING_EFFORT_BY_PROVIDER = { codex: "high", + claudeCode: null, + cursor: null, } as const satisfies Record; diff --git a/packages/shared/src/model.test.ts b/packages/shared/src/model.test.ts index 8771a24c1..224b86e56 100644 --- a/packages/shared/src/model.test.ts +++ b/packages/shared/src/model.test.ts @@ -1,13 +1,26 @@ import { describe, expect, it } from "vitest"; -import { DEFAULT_MODEL_BY_PROVIDER, MODEL_OPTIONS_BY_PROVIDER } from "@t3tools/contracts"; +import { + CURSOR_MODEL_FAMILY_OPTIONS, + CURSOR_REASONING_OPTIONS, + DEFAULT_MODEL, + DEFAULT_MODEL_BY_PROVIDER, + DEFAULT_REASONING_EFFORT_BY_PROVIDER, + MODEL_OPTIONS, + MODEL_OPTIONS_BY_PROVIDER, + REASONING_EFFORT_OPTIONS_BY_PROVIDER, +} from "@t3tools/contracts"; import { getDefaultModel, getDefaultReasoningEffort, + getCursorModelFamilyOptions, getModelOptions, getReasoningEffortOptions, normalizeModelSlug, + parseCursorModelSelection, + resolveCursorModelFromSelection, resolveModelSlug, + resolveModelSlugForProvider, } from "./model"; describe("normalizeModelSlug", () => { @@ -32,38 +45,141 @@ describe("normalizeModelSlug", () => { expect(normalizeModelSlug("toString")).toBe("toString"); expect(normalizeModelSlug("constructor")).toBe("constructor"); }); + + it("uses provider-specific aliases", () => { + expect(normalizeModelSlug("sonnet", "claudeCode")).toBe("claude-sonnet-4-6"); + expect(normalizeModelSlug("opus-4.6", "claudeCode")).toBe("claude-opus-4-6"); + expect(normalizeModelSlug("claude-haiku-4-5-20251001", "claudeCode")).toBe( + "claude-haiku-4-5", + ); + expect(normalizeModelSlug("composer", "cursor")).toBe("composer-1.5"); + expect(normalizeModelSlug("gpt-5.3-codex-spark", "cursor")).toBe( + "gpt-5.3-codex-spark-preview", + ); + expect(normalizeModelSlug("gemini-3.1", "cursor")).toBe("gemini-3.1"); + expect(normalizeModelSlug("claude-4.6-sonnet-thinking", "cursor")).toBe( + "sonnet-4.6-thinking", + ); + }); }); describe("resolveModelSlug", () => { it("returns default only when the model is missing", () => { - expect(resolveModelSlug(undefined)).toBe(DEFAULT_MODEL_BY_PROVIDER.codex); - expect(resolveModelSlug(null)).toBe(DEFAULT_MODEL_BY_PROVIDER.codex); + expect(resolveModelSlug(undefined)).toBe(DEFAULT_MODEL); + expect(resolveModelSlug(null)).toBe(DEFAULT_MODEL); }); it("preserves unknown custom models", () => { - expect(resolveModelSlug("gpt-4.1")).toBe(DEFAULT_MODEL_BY_PROVIDER.codex); - expect(resolveModelSlug("custom/internal-model")).toBe(DEFAULT_MODEL_BY_PROVIDER.codex); + expect(resolveModelSlug("gpt-4.1")).toBe(DEFAULT_MODEL); + expect(resolveModelSlug("custom/internal-model")).toBe(DEFAULT_MODEL); }); it("resolves only supported model options", () => { - for (const model of MODEL_OPTIONS_BY_PROVIDER.codex) { + for (const model of MODEL_OPTIONS) { expect(resolveModelSlug(model.slug)).toBe(model.slug); } }); + + it("supports provider-aware resolution", () => { + expect(resolveModelSlugForProvider("claudeCode", undefined)).toBe( + DEFAULT_MODEL_BY_PROVIDER.claudeCode, + ); + expect(resolveModelSlugForProvider("claudeCode", "sonnet")).toBe("claude-sonnet-4-6"); + expect(resolveModelSlugForProvider("claudeCode", "gpt-5.3-codex")).toBe( + DEFAULT_MODEL_BY_PROVIDER.claudeCode, + ); + expect(resolveModelSlugForProvider("cursor", undefined)).toBe( + DEFAULT_MODEL_BY_PROVIDER.cursor, + ); + expect(resolveModelSlugForProvider("cursor", "composer")).toBe("composer-1.5"); + expect(resolveModelSlugForProvider("cursor", "gpt-5.3-codex-high-fast")).toBe( + "gpt-5.3-codex-high-fast", + ); + expect(resolveModelSlugForProvider("cursor", "claude-sonnet-4-6")).toBe( + DEFAULT_MODEL_BY_PROVIDER.cursor, + ); + }); + it("keeps codex defaults for backward compatibility", () => { - expect(getDefaultModel()).toBe(DEFAULT_MODEL_BY_PROVIDER.codex); - expect(getModelOptions()).toEqual(MODEL_OPTIONS_BY_PROVIDER.codex); + expect(getDefaultModel()).toBe(DEFAULT_MODEL); + expect(getModelOptions()).toEqual(MODEL_OPTIONS); + expect(getModelOptions("claudeCode")).toEqual(MODEL_OPTIONS_BY_PROVIDER.claudeCode); + expect(getModelOptions("cursor")).toEqual(MODEL_OPTIONS_BY_PROVIDER.cursor); + expect(getCursorModelFamilyOptions()).toEqual(CURSOR_MODEL_FAMILY_OPTIONS); + }); +}); + +describe("cursor model selection", () => { + it("includes the expected cursor reasoning levels and families", () => { + expect(CURSOR_REASONING_OPTIONS).toEqual(["low", "normal", "high", "xhigh"]); + expect(getCursorModelFamilyOptions().map((option) => option.slug)).toContain("gpt-5.3-codex"); + expect(getCursorModelFamilyOptions().map((option) => option.slug)).toContain("opus-4.6"); + }); + + it("parses codex reasoning and fast mode variants", () => { + expect(parseCursorModelSelection("gpt-5.3-codex-high-fast")).toEqual({ + family: "gpt-5.3-codex", + reasoning: "high", + fast: true, + thinking: false, + }); + expect(parseCursorModelSelection("gpt-5.2-codex")).toEqual( + parseCursorModelSelection(DEFAULT_MODEL_BY_PROVIDER.cursor), + ); + }); + + it("parses and resolves thinking variants", () => { + expect(parseCursorModelSelection("sonnet-4.6-thinking")).toEqual({ + family: "sonnet-4.6", + reasoning: "normal", + fast: false, + thinking: true, + }); + expect( + resolveCursorModelFromSelection({ + family: "sonnet-4.6", + thinking: true, + }), + ).toBe("sonnet-4.6-thinking"); + }); + + it("resolves codex family selections into concrete model ids", () => { + expect( + resolveCursorModelFromSelection({ + family: "gpt-5.3-codex", + reasoning: "xhigh", + fast: true, + }), + ).toBe("gpt-5.3-codex-xhigh-fast"); }); }); describe("getReasoningEffortOptions", () => { it("returns codex reasoning options for codex", () => { - expect(getReasoningEffortOptions("codex")).toEqual(["xhigh", "high", "medium", "low"]); + expect(getReasoningEffortOptions("codex")).toEqual( + REASONING_EFFORT_OPTIONS_BY_PROVIDER.codex, + ); + }); + + it("returns no reasoning options for claudeCode", () => { + expect(getReasoningEffortOptions("claudeCode")).toEqual([]); + }); + + it("returns no reasoning options for cursor", () => { + expect(getReasoningEffortOptions("cursor")).toEqual([]); }); }); describe("getDefaultReasoningEffort", () => { it("returns provider-scoped defaults", () => { - expect(getDefaultReasoningEffort("codex")).toBe("high"); + expect(getDefaultReasoningEffort("codex")).toBe( + DEFAULT_REASONING_EFFORT_BY_PROVIDER.codex, + ); + expect(getDefaultReasoningEffort("claudeCode")).toBe( + DEFAULT_REASONING_EFFORT_BY_PROVIDER.claudeCode, + ); + expect(getDefaultReasoningEffort("cursor")).toBe( + DEFAULT_REASONING_EFFORT_BY_PROVIDER.cursor, + ); }); }); diff --git a/packages/shared/src/model.ts b/packages/shared/src/model.ts index 592e2dfb9..268491a84 100644 --- a/packages/shared/src/model.ts +++ b/packages/shared/src/model.ts @@ -1,23 +1,227 @@ import { CODEX_REASONING_EFFORT_OPTIONS, + CURSOR_MODEL_FAMILY_OPTIONS, + CURSOR_REASONING_OPTIONS, DEFAULT_MODEL_BY_PROVIDER, + DEFAULT_REASONING_EFFORT_BY_PROVIDER, MODEL_OPTIONS_BY_PROVIDER, MODEL_SLUG_ALIASES_BY_PROVIDER, + REASONING_EFFORT_OPTIONS_BY_PROVIDER, type CodexReasoningEffort, + type CursorModelFamily, + type CursorModelSlug, + type CursorReasoningOption, type ModelSlug, type ProviderKind, } from "@t3tools/contracts"; -type CatalogProvider = keyof typeof MODEL_OPTIONS_BY_PROVIDER; +type CursorModelCapability = { + readonly supportsReasoning: boolean; + readonly supportsFast: boolean; + readonly supportsThinking: boolean; + readonly defaultReasoning: CursorReasoningOption; + readonly defaultThinking: boolean; +}; + +const CURSOR_MODEL_CAPABILITY_BY_FAMILY: Record = { + auto: { + supportsReasoning: false, + supportsFast: false, + supportsThinking: false, + defaultReasoning: "normal", + defaultThinking: false, + }, + "composer-1.5": { + supportsReasoning: false, + supportsFast: false, + supportsThinking: false, + defaultReasoning: "normal", + defaultThinking: false, + }, + "composer-1": { + supportsReasoning: false, + supportsFast: false, + supportsThinking: false, + defaultReasoning: "normal", + defaultThinking: false, + }, + "gpt-5.3-codex": { + supportsReasoning: true, + supportsFast: true, + supportsThinking: false, + defaultReasoning: "normal", + defaultThinking: false, + }, + "gpt-5.3-codex-spark-preview": { + supportsReasoning: false, + supportsFast: false, + supportsThinking: false, + defaultReasoning: "normal", + defaultThinking: false, + }, + "opus-4.6": { + supportsReasoning: false, + supportsFast: false, + supportsThinking: true, + defaultReasoning: "normal", + defaultThinking: true, + }, + "opus-4.5": { + supportsReasoning: false, + supportsFast: false, + supportsThinking: true, + defaultReasoning: "normal", + defaultThinking: true, + }, + "sonnet-4.6": { + supportsReasoning: false, + supportsFast: false, + supportsThinking: true, + defaultReasoning: "normal", + defaultThinking: true, + }, + "gemini-3.1-pro": { + supportsReasoning: false, + supportsFast: false, + supportsThinking: false, + defaultReasoning: "normal", + defaultThinking: false, + }, +}; -const MODEL_SLUG_SET_BY_PROVIDER: Record> = { +const MODEL_SLUG_SET_BY_PROVIDER: Record> = { + claudeCode: new Set(MODEL_OPTIONS_BY_PROVIDER.claudeCode.map((option) => option.slug)), codex: new Set(MODEL_OPTIONS_BY_PROVIDER.codex.map((option) => option.slug)), + cursor: new Set(MODEL_OPTIONS_BY_PROVIDER.cursor.map((option) => option.slug)), }; +const CURSOR_MODEL_FAMILY_SET = new Set( + CURSOR_MODEL_FAMILY_OPTIONS.map((option) => option.slug), +); + +export interface CursorModelSelection { + readonly family: CursorModelFamily; + readonly reasoning: CursorReasoningOption; + readonly fast: boolean; + readonly thinking: boolean; +} + export function getModelOptions(provider: ProviderKind = "codex") { return MODEL_OPTIONS_BY_PROVIDER[provider]; } +export function getCursorModelFamilyOptions() { + return CURSOR_MODEL_FAMILY_OPTIONS; +} + +export function getCursorModelCapabilities(family: CursorModelFamily) { + return CURSOR_MODEL_CAPABILITY_BY_FAMILY[family]; +} + +function fallbackCursorModelFamily(): CursorModelFamily { + const fallback = parseCursorModelSelection(DEFAULT_MODEL_BY_PROVIDER.cursor); + return fallback.family; +} + +function resolveCursorModelFamily(model: string | null | undefined): CursorModelFamily { + const normalized = normalizeModelSlug(model, "cursor"); + if (!normalized) { + return fallbackCursorModelFamily(); + } + + if ( + normalized === "gpt-5.3-codex" || + normalized === "gpt-5.3-codex-fast" || + normalized === "gpt-5.3-codex-low" || + normalized === "gpt-5.3-codex-low-fast" || + normalized === "gpt-5.3-codex-high" || + normalized === "gpt-5.3-codex-high-fast" || + normalized === "gpt-5.3-codex-xhigh" || + normalized === "gpt-5.3-codex-xhigh-fast" + ) { + return "gpt-5.3-codex"; + } + + if (normalized === "sonnet-4.6-thinking") { + return "sonnet-4.6"; + } + if (normalized === "opus-4.6-thinking") { + return "opus-4.6"; + } + if (normalized === "opus-4.5-thinking") { + return "opus-4.5"; + } + + return CURSOR_MODEL_FAMILY_SET.has(normalized as CursorModelFamily) + ? (normalized as CursorModelFamily) + : fallbackCursorModelFamily(); +} + +function resolveCursorReasoning(model: CursorModelSlug): CursorReasoningOption { + if (model.includes("-xhigh")) return "xhigh"; + if (model.includes("-high")) return "high"; + if (model.includes("-low")) return "low"; + return "normal"; +} + +export function parseCursorModelSelection(model: string | null | undefined): CursorModelSelection { + const family = resolveCursorModelFamily(model); + const capability = CURSOR_MODEL_CAPABILITY_BY_FAMILY[family]; + const normalized = resolveModelSlugForProvider("cursor", model) as CursorModelSlug; + + if (capability.supportsReasoning) { + return { + family, + reasoning: resolveCursorReasoning(normalized), + fast: normalized.endsWith("-fast"), + thinking: false, + }; + } + + if (capability.supportsThinking) { + return { + family, + reasoning: capability.defaultReasoning, + fast: false, + thinking: normalized.endsWith("-thinking"), + }; + } + + return { + family, + reasoning: capability.defaultReasoning, + fast: false, + thinking: capability.defaultThinking, + }; +} + +export function resolveCursorModelFromSelection(input: { + readonly family: CursorModelFamily; + readonly reasoning?: CursorReasoningOption | null; + readonly fast?: boolean; + readonly thinking?: boolean; +}): CursorModelSlug { + const family = resolveCursorModelFamily(input.family); + const capability = CURSOR_MODEL_CAPABILITY_BY_FAMILY[family]; + + if (capability.supportsReasoning) { + const reasoning = CURSOR_REASONING_OPTIONS.includes(input.reasoning ?? "normal") + ? (input.reasoning ?? "normal") + : capability.defaultReasoning; + const reasoningSuffix = reasoning === "normal" ? "" : `-${reasoning}`; + const fastSuffix = input.fast ? "-fast" : ""; + const candidate = `${family}${reasoningSuffix}${fastSuffix}`; + return resolveModelSlugForProvider("cursor", candidate) as CursorModelSlug; + } + + if (capability.supportsThinking) { + const candidate = input.thinking ? `${family}-thinking` : family; + return resolveModelSlugForProvider("cursor", candidate) as CursorModelSlug; + } + + return resolveModelSlugForProvider("cursor", family) as CursorModelSlug; +} + export function getDefaultModel(provider: ProviderKind = "codex"): ModelSlug { return DEFAULT_MODEL_BY_PROVIDER[provider]; } @@ -36,7 +240,9 @@ export function normalizeModelSlug( } const aliases = MODEL_SLUG_ALIASES_BY_PROVIDER[provider] as Record; - const aliased = aliases[trimmed]; + const aliased = Object.prototype.hasOwnProperty.call(aliases, trimmed) + ? aliases[trimmed] + : undefined; return typeof aliased === "string" ? aliased : (trimmed as ModelSlug); } @@ -46,12 +252,12 @@ export function resolveModelSlug( ): ModelSlug { const normalized = normalizeModelSlug(model, provider); if (!normalized) { - return getDefaultModel(provider); + return DEFAULT_MODEL_BY_PROVIDER[provider]; } return MODEL_SLUG_SET_BY_PROVIDER[provider].has(normalized) ? normalized - : getDefaultModel(provider); + : DEFAULT_MODEL_BY_PROVIDER[provider]; } export function resolveModelSlugForProvider( @@ -64,7 +270,7 @@ export function resolveModelSlugForProvider( export function getReasoningEffortOptions( provider: ProviderKind = "codex", ): ReadonlyArray { - return provider === "codex" ? CODEX_REASONING_EFFORT_OPTIONS : []; + return REASONING_EFFORT_OPTIONS_BY_PROVIDER[provider]; } export function getDefaultReasoningEffort(provider: "codex"): CodexReasoningEffort; @@ -72,7 +278,7 @@ export function getDefaultReasoningEffort(provider: ProviderKind): CodexReasonin export function getDefaultReasoningEffort( provider: ProviderKind = "codex", ): CodexReasoningEffort | null { - return provider === "codex" ? "high" : null; + return DEFAULT_REASONING_EFFORT_BY_PROVIDER[provider]; } export { CODEX_REASONING_EFFORT_OPTIONS }; From 1da30f776e418a45c9d16c0fcb12b2b0397e4d28 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Mar 2026 21:33:48 -0800 Subject: [PATCH 05/23] split: restore Claude provider surface on sibling stack Co-authored-by: codex --- .../OrchestrationEngineHarness.integration.ts | 2 +- .../TestProviderAdapter.integration.ts | 10 +- .../orchestrationEngine.integration.test.ts | 415 +++++++++++++++++- .../Layers/CheckpointReactor.test.ts | 2 +- .../Layers/ProviderCommandReactor.ts | 6 +- .../Layers/ProviderRuntimeIngestion.test.ts | 2 +- .../src/provider/Layers/CodexAdapter.test.ts | 23 + .../provider/Layers/ProviderService.test.ts | 2 +- .../Layers/ProviderSessionDirectory.ts | 2 +- apps/web/src/appSettings.test.ts | 28 ++ apps/web/src/appSettings.ts | 22 +- apps/web/src/components/ChatView.tsx | 3 + apps/web/src/composerDraftStore.ts | 2 +- apps/web/src/routes/_chat.settings.tsx | 14 + apps/web/src/store.test.ts | 48 +- apps/web/src/store.ts | 34 +- packages/contracts/src/orchestration.test.ts | 4 +- packages/contracts/src/orchestration.ts | 14 +- .../contracts/src/providerRuntime.test.ts | 6 +- packages/contracts/src/providerRuntime.ts | 2 + 20 files changed, 613 insertions(+), 28 deletions(-) diff --git a/apps/server/integration/OrchestrationEngineHarness.integration.ts b/apps/server/integration/OrchestrationEngineHarness.integration.ts index c5eb125ab..642f9b3de 100644 --- a/apps/server/integration/OrchestrationEngineHarness.integration.ts +++ b/apps/server/integration/OrchestrationEngineHarness.integration.ts @@ -205,7 +205,7 @@ export interface OrchestrationIntegrationHarness { } interface MakeOrchestrationIntegrationHarnessOptions { - readonly provider?: "codex"; + readonly provider?: "codex" | "claudeCode"; readonly realCodex?: boolean; } diff --git a/apps/server/integration/TestProviderAdapter.integration.ts b/apps/server/integration/TestProviderAdapter.integration.ts index 017c59e2c..d970bf784 100644 --- a/apps/server/integration/TestProviderAdapter.integration.ts +++ b/apps/server/integration/TestProviderAdapter.integration.ts @@ -35,7 +35,7 @@ export interface TestTurnResponse { export type FixtureProviderRuntimeEvent = { readonly type: string; readonly eventId: EventId; - readonly provider: "codex"; + readonly provider: "codex" | "claudeCode" | "cursor"; readonly createdAt: string; readonly threadId: string; readonly turnId?: string | undefined; @@ -177,7 +177,7 @@ function normalizeFixtureEvent(rawEvent: Record): ProviderRunti export interface TestProviderAdapterHarness { readonly adapter: ProviderAdapterShape; - readonly provider: "codex"; + readonly provider: "codex" | "claudeCode"; readonly queueTurnResponse: ( threadId: ThreadId, response: TestTurnResponse, @@ -197,7 +197,7 @@ export interface TestProviderAdapterHarness { } interface MakeTestProviderAdapterHarnessOptions { - readonly provider?: "codex"; + readonly provider?: "codex" | "claudeCode"; } function nowIso(): string { @@ -205,7 +205,7 @@ function nowIso(): string { } function sessionNotFound( - provider: "codex", + provider: "codex" | "claudeCode", threadId: ThreadId, ): ProviderAdapterSessionNotFoundError { return new ProviderAdapterSessionNotFoundError({ @@ -215,7 +215,7 @@ function sessionNotFound( } function missingSessionEffect( - provider: "codex", + provider: "codex" | "claudeCode", threadId: ThreadId, ): Effect.Effect { return Effect.fail(sessionNotFound(provider, threadId)); diff --git a/apps/server/integration/orchestrationEngine.integration.test.ts b/apps/server/integration/orchestrationEngine.integration.test.ts index 42dcfe34f..ab155b91d 100644 --- a/apps/server/integration/orchestrationEngine.integration.test.ts +++ b/apps/server/integration/orchestrationEngine.integration.test.ts @@ -36,7 +36,7 @@ const PROJECT_ID = asProjectId("project-1"); const THREAD_ID = ThreadId.makeUnsafe("thread-1"); const FIXTURE_TURN_ID = "fixture-turn"; const APPROVAL_REQUEST_ID = asApprovalRequestId("req-approval-1"); -type IntegrationProvider = "codex"; +type IntegrationProvider = "codex" | "claudeCode"; function nowIso() { return new Date().toISOString(); @@ -882,3 +882,416 @@ it.live( }), ), ); + +it.live("starts a claudeCode session on first turn when provider is requested", () => + withHarness( + (harness) => + Effect.gen(function* () { + yield* seedProjectAndThread(harness); + + yield* harness.adapterHarness.queueTurnResponseForNextSession({ + events: [ + { + type: "turn.started", + ...runtimeBase("evt-claude-start-1", "2026-02-24T10:10:00.000Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + }, + { + type: "message.delta", + ...runtimeBase("evt-claude-start-2", "2026-02-24T10:10:00.050Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + delta: "Claude first turn.\n", + }, + { + type: "turn.completed", + ...runtimeBase("evt-claude-start-3", "2026-02-24T10:10:00.100Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + status: "completed", + }, + ], + }); + + yield* startTurn({ + harness, + commandId: "cmd-turn-start-claude-initial", + messageId: "msg-user-claude-initial", + text: "Use Claude", + provider: "claudeCode", + }); + + const thread = yield* harness.waitForThread( + THREAD_ID, + (entry) => + entry.session?.providerName === "claudeCode" && + entry.session.status === "ready" && + entry.messages.some( + (message) => message.role === "assistant" && message.text === "Claude first turn.\n", + ), + ); + assert.equal(thread.session?.providerName, "claudeCode"); + }), + "claudeCode", + ), +); + +it.live("recovers claudeCode sessions after provider stopAll using persisted resume state", () => + withHarness( + (harness) => + Effect.gen(function* () { + yield* seedProjectAndThread(harness); + + yield* harness.adapterHarness.queueTurnResponseForNextSession({ + events: [ + { + type: "turn.started", + ...runtimeBase("evt-claude-recover-1", "2026-02-24T10:11:00.000Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + }, + { + type: "message.delta", + ...runtimeBase("evt-claude-recover-2", "2026-02-24T10:11:00.050Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + delta: "Turn before restart.\n", + }, + { + type: "turn.completed", + ...runtimeBase("evt-claude-recover-3", "2026-02-24T10:11:00.100Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + status: "completed", + }, + ], + }); + + yield* startTurn({ + harness, + commandId: "cmd-turn-start-claude-recover-1", + messageId: "msg-user-claude-recover-1", + text: "Before restart", + provider: "claudeCode", + }); + + yield* harness.waitForThread( + THREAD_ID, + (entry) => entry.latestTurn?.turnId === "turn-1" && entry.session?.threadId === "thread-1", + ); + + yield* harness.providerService.stopAll(); + yield* waitForSync( + () => harness.adapterHarness.listActiveSessionIds(), + (sessionIds) => sessionIds.length === 0, + "provider stopAll", + ); + + yield* harness.adapterHarness.queueTurnResponseForNextSession({ + events: [ + { + type: "turn.started", + ...runtimeBase("evt-claude-recover-4", "2026-02-24T10:11:01.000Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + }, + { + type: "message.delta", + ...runtimeBase("evt-claude-recover-5", "2026-02-24T10:11:01.050Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + delta: "Turn after restart.\n", + }, + { + type: "turn.completed", + ...runtimeBase("evt-claude-recover-6", "2026-02-24T10:11:01.100Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + status: "completed", + }, + ], + }); + + yield* startTurn({ + harness, + commandId: "cmd-turn-start-claude-recover-2", + messageId: "msg-user-claude-recover-2", + text: "After restart", + }); + yield* waitForSync( + () => harness.adapterHarness.getStartCount(), + (count) => count === 2, + "claude provider recovery start", + ); + + const recoveredThread = yield* harness.waitForThread( + THREAD_ID, + (entry) => + entry.session?.providerName === "claudeCode" && + entry.messages.some( + (message) => message.role === "user" && message.text === "After restart", + ) && + !entry.activities.some((activity) => activity.kind === "provider.turn.start.failed"), + ); + assert.equal(recoveredThread.session?.providerName, "claudeCode"); + assert.equal(recoveredThread.session?.threadId, "thread-1"); + }), + "claudeCode", + ), +); + +it.live("forwards claudeCode approval responses to the provider session", () => + withHarness( + (harness) => + Effect.gen(function* () { + yield* seedProjectAndThread(harness); + + yield* harness.adapterHarness.queueTurnResponseForNextSession({ + events: [ + { + type: "turn.started", + ...runtimeBase("evt-claude-approval-1", "2026-02-24T10:12:00.000Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + }, + { + type: "approval.requested", + ...runtimeBase("evt-claude-approval-2", "2026-02-24T10:12:00.050Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + requestId: APPROVAL_REQUEST_ID, + requestKind: "command", + detail: "Approve Claude tool call", + }, + { + type: "turn.completed", + ...runtimeBase("evt-claude-approval-3", "2026-02-24T10:12:00.100Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + status: "completed", + }, + ], + }); + + yield* startTurn({ + harness, + commandId: "cmd-turn-start-claude-approval", + messageId: "msg-user-claude-approval", + text: "Need approval", + provider: "claudeCode", + }); + + const thread = yield* harness.waitForThread(THREAD_ID, (entry) => + entry.activities.some((activity) => activity.kind === "approval.requested"), + ); + assert.equal(thread.session?.threadId, "thread-1"); + + yield* harness.engine.dispatch({ + type: "thread.approval.respond", + commandId: CommandId.makeUnsafe("cmd-claude-approval-respond"), + threadId: THREAD_ID, + requestId: APPROVAL_REQUEST_ID, + decision: "accept", + createdAt: nowIso(), + }); + + yield* harness.waitForPendingApproval( + "req-approval-1", + (row) => row.status === "resolved" && row.decision === "accept", + ); + + const approvalResponses = yield* waitForSync( + () => harness.adapterHarness.getApprovalResponses(THREAD_ID), + (responses) => responses.length === 1, + "claude provider approval response", + ); + assert.equal(approvalResponses[0]?.decision, "accept"); + }), + "claudeCode", + ), +); + +it.live("forwards thread.turn.interrupt to claudeCode provider sessions", () => + withHarness( + (harness) => + Effect.gen(function* () { + yield* seedProjectAndThread(harness); + + yield* harness.adapterHarness.queueTurnResponseForNextSession({ + events: [ + { + type: "turn.started", + ...runtimeBase("evt-claude-interrupt-1", "2026-02-24T10:13:00.000Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + }, + { + type: "message.delta", + ...runtimeBase("evt-claude-interrupt-2", "2026-02-24T10:13:00.050Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + delta: "Long running output.\n", + }, + { + type: "turn.completed", + ...runtimeBase("evt-claude-interrupt-3", "2026-02-24T10:13:00.100Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + status: "completed", + }, + ], + }); + + yield* startTurn({ + harness, + commandId: "cmd-turn-start-claude-interrupt", + messageId: "msg-user-claude-interrupt", + text: "Start long turn", + provider: "claudeCode", + }); + + const thread = yield* harness.waitForThread( + THREAD_ID, + (entry) => entry.session?.threadId === "thread-1", + ); + assert.equal(thread.session?.threadId, "thread-1"); + + yield* harness.engine.dispatch({ + type: "thread.turn.interrupt", + commandId: CommandId.makeUnsafe("cmd-turn-interrupt-claude"), + threadId: THREAD_ID, + createdAt: nowIso(), + }); + yield* harness.waitForDomainEvent((event) => event.type === "thread.turn-interrupt-requested"); + + const interruptCalls = yield* waitForSync( + () => harness.adapterHarness.getInterruptCalls(THREAD_ID), + (calls) => calls.length === 1, + "claude provider interrupt call", + ); + assert.equal(interruptCalls.length, 1); + }), + "claudeCode", + ), +); + +it.live("reverts claudeCode turns and rolls back provider conversation state", () => + withHarness( + (harness) => + Effect.gen(function* () { + yield* seedProjectAndThread(harness); + + yield* harness.adapterHarness.queueTurnResponseForNextSession({ + events: [ + { + type: "turn.started", + ...runtimeBase("evt-claude-revert-1", "2026-02-24T10:14:00.000Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + }, + { + type: "message.delta", + ...runtimeBase("evt-claude-revert-2", "2026-02-24T10:14:00.050Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + delta: "README -> v2\n", + }, + { + type: "turn.completed", + ...runtimeBase("evt-claude-revert-3", "2026-02-24T10:14:00.100Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + status: "completed", + }, + ], + mutateWorkspace: ({ cwd }) => + Effect.sync(() => { + fs.writeFileSync(path.join(cwd, "README.md"), "v2\n", "utf8"); + }), + }); + + yield* startTurn({ + harness, + commandId: "cmd-turn-start-claude-revert-1", + messageId: "msg-user-claude-revert-1", + text: "First Claude edit", + provider: "claudeCode", + }); + + yield* harness.waitForThread( + THREAD_ID, + (entry) => entry.latestTurn?.turnId === "turn-1" && entry.session?.threadId === "thread-1", + ); + + yield* harness.adapterHarness.queueTurnResponse(THREAD_ID, { + events: [ + { + type: "turn.started", + ...runtimeBase("evt-claude-revert-4", "2026-02-24T10:14:01.000Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + }, + { + type: "message.delta", + ...runtimeBase("evt-claude-revert-5", "2026-02-24T10:14:01.050Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + delta: "README -> v3\n", + }, + { + type: "turn.completed", + ...runtimeBase("evt-claude-revert-6", "2026-02-24T10:14:01.100Z", "claudeCode"), + threadId: THREAD_ID, + turnId: FIXTURE_TURN_ID, + status: "completed", + }, + ], + mutateWorkspace: ({ cwd }) => + Effect.sync(() => { + fs.writeFileSync(path.join(cwd, "README.md"), "v3\n", "utf8"); + }), + }); + + yield* startTurn({ + harness, + commandId: "cmd-turn-start-claude-revert-2", + messageId: "msg-user-claude-revert-2", + text: "Second Claude edit", + }); + + yield* harness.waitForThread( + THREAD_ID, + (entry) => + entry.latestTurn?.turnId === "turn-2" && + entry.checkpoints.length === 2 && + entry.session?.providerName === "claudeCode", + ); + + yield* harness.engine.dispatch({ + type: "thread.checkpoint.revert", + commandId: CommandId.makeUnsafe("cmd-checkpoint-revert-claude"), + threadId: THREAD_ID, + turnCount: 1, + createdAt: nowIso(), + }); + + const revertedThread = yield* harness.waitForThread( + THREAD_ID, + (entry) => + entry.checkpoints.length === 1 && entry.checkpoints[0]?.checkpointTurnCount === 1, + ); + assert.equal(revertedThread.checkpoints[0]?.checkpointTurnCount, 1); + assert.equal( + gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 1)), + true, + ); + assert.equal( + gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 2)), + false, + ); + assert.deepEqual(harness.adapterHarness.getRollbackCalls(THREAD_ID), [1]); + }), + "claudeCode", + ), +); diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts index 346ca7b33..2944012f0 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts @@ -44,7 +44,7 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); type LegacyProviderRuntimeEvent = { readonly type: string; readonly eventId: EventId; - readonly provider: "codex"; + readonly provider: "codex" | "claudeCode" | "cursor"; readonly createdAt: string; readonly threadId: ThreadId; readonly turnId?: string | undefined; diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index fe0218845..eb976f5d1 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -206,7 +206,11 @@ const make = Effect.gen(function* () { const desiredRuntimeMode = thread.runtimeMode; const currentProvider: ProviderKind | undefined = - thread.session?.providerName === "codex" ? thread.session.providerName : undefined; + thread.session?.providerName === "codex" || + thread.session?.providerName === "claudeCode" || + thread.session?.providerName === "cursor" + ? thread.session.providerName + : undefined; const preferredProvider: ProviderKind | undefined = options?.provider ?? currentProvider; const desiredModel = options?.model ?? thread.model; const effectiveCwd = resolveThreadWorkspaceCwd({ diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts index 525cd0214..63be516d0 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts @@ -45,7 +45,7 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); type LegacyProviderRuntimeEvent = { readonly type: string; readonly eventId: EventId; - readonly provider: "codex"; + readonly provider: "codex" | "claudeCode" | "cursor"; readonly createdAt: string; readonly threadId: ThreadId; readonly turnId?: string | undefined; diff --git a/apps/server/src/provider/Layers/CodexAdapter.test.ts b/apps/server/src/provider/Layers/CodexAdapter.test.ts index 3ad206d0b..bc299fbc8 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.test.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.test.ts @@ -156,6 +156,29 @@ const validationLayer = it.layer( ); validationLayer("CodexAdapterLive validation", (it) => { + it.effect("returns validation error for non-codex provider on startSession", () => + Effect.gen(function* () { + const adapter = yield* CodexAdapter; + const result = yield* adapter + .startSession({ + provider: "claudeCode", + threadId: asThreadId("thread-1"), + runtimeMode: "full-access", + }) + .pipe(Effect.result); + + assert.equal(result._tag, "Failure"); + assert.deepStrictEqual( + result.failure, + new ProviderAdapterValidationError({ + provider: "codex", + operation: "startSession", + issue: "Expected provider 'codex' but received 'claudeCode'.", + }), + ); + assert.equal(validationManager.startSessionImpl.mock.calls.length, 0); + }), + ); it.effect("maps codex model options before starting a session", () => Effect.gen(function* () { validationManager.startSessionImpl.mockClear(); diff --git a/apps/server/src/provider/Layers/ProviderService.test.ts b/apps/server/src/provider/Layers/ProviderService.test.ts index 828dd5b0f..10b3c9558 100644 --- a/apps/server/src/provider/Layers/ProviderService.test.ts +++ b/apps/server/src/provider/Layers/ProviderService.test.ts @@ -52,7 +52,7 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); type LegacyProviderRuntimeEvent = { readonly type: string; readonly eventId: EventId; - readonly provider: "codex"; + readonly provider: "codex" | "claudeCode" | "cursor"; readonly createdAt: string; readonly threadId: ThreadId; readonly turnId?: string | undefined; diff --git a/apps/server/src/provider/Layers/ProviderSessionDirectory.ts b/apps/server/src/provider/Layers/ProviderSessionDirectory.ts index 38e097e1c..3f675aae8 100644 --- a/apps/server/src/provider/Layers/ProviderSessionDirectory.ts +++ b/apps/server/src/provider/Layers/ProviderSessionDirectory.ts @@ -22,7 +22,7 @@ function decodeProviderKind( providerName: string, operation: string, ): Effect.Effect { - if (providerName === "codex") { + if (providerName === "codex" || providerName === "claudeCode" || providerName === "cursor") { return Effect.succeed(providerName); } return Effect.fail( diff --git a/apps/web/src/appSettings.test.ts b/apps/web/src/appSettings.test.ts index 326bceaac..a82e972bc 100644 --- a/apps/web/src/appSettings.test.ts +++ b/apps/web/src/appSettings.test.ts @@ -20,6 +20,17 @@ describe("normalizeCustomModelSlugs", () => { ]), ).toEqual(["custom/internal-model"]); }); + + it("normalizes provider-specific aliases for claude and cursor", () => { + expect(normalizeCustomModelSlugs(["sonnet"], "claudeCode")).toEqual([]); + expect(normalizeCustomModelSlugs(["claude/custom-sonnet"], "claudeCode")).toEqual([ + "claude/custom-sonnet", + ]); + expect(normalizeCustomModelSlugs(["composer"], "cursor")).toEqual([]); + expect(normalizeCustomModelSlugs(["cursor/custom-model"], "cursor")).toEqual([ + "cursor/custom-model", + ]); + }); }); describe("getAppModelOptions", () => { @@ -45,6 +56,13 @@ describe("getAppModelOptions", () => { isCustom: true, }); }); + it("keeps a saved custom provider model available as an exact slug option", () => { + const options = getAppModelOptions("claudeCode", ["claude/custom-opus"], "claude/custom-opus"); + + expect(options.some((option) => option.slug === "claude/custom-opus" && option.isCustom)).toBe( + true, + ); + }); }); describe("resolveAppModelSelection", () => { @@ -64,3 +82,13 @@ describe("timestamp format defaults", () => { expect(DEFAULT_TIMESTAMP_FORMAT).toBe("locale"); }); }); + +describe("provider-specific custom models", () => { + it("includes provider-specific custom slugs in non-codex model lists", () => { + const claudeOptions = getAppModelOptions("claudeCode", ["claude/custom-opus"]); + const cursorOptions = getAppModelOptions("cursor", ["cursor/custom-model"]); + + expect(claudeOptions.some((option) => option.slug === "claude/custom-opus")).toBe(true); + expect(cursorOptions.some((option) => option.slug === "cursor/custom-model")).toBe(true); + }); +}); diff --git a/apps/web/src/appSettings.ts b/apps/web/src/appSettings.ts index 18e76d2f9..5157addd5 100644 --- a/apps/web/src/appSettings.ts +++ b/apps/web/src/appSettings.ts @@ -12,6 +12,8 @@ export type TimestampFormat = (typeof TIMESTAMP_FORMAT_OPTIONS)[number]; export const DEFAULT_TIMESTAMP_FORMAT: TimestampFormat = "locale"; const BUILT_IN_MODEL_SLUGS_BY_PROVIDER: Record> = { codex: new Set(getModelOptions("codex").map((option) => option.slug)), + claudeCode: new Set(getModelOptions("claudeCode").map((option) => option.slug)), + cursor: new Set(getModelOptions("cursor").map((option) => option.slug)), }; const AppSettingsSchema = Schema.Struct({ @@ -31,6 +33,13 @@ const AppSettingsSchema = Schema.Struct({ timestampFormat: Schema.Literals(["locale", "12-hour", "24-hour"]).pipe( Schema.withConstructorDefault(() => Option.some(DEFAULT_TIMESTAMP_FORMAT)), ), + customCodexModels: Schema.Array(Schema.String).pipe( + customClaudeModels: Schema.Array(Schema.String).pipe( + Schema.withConstructorDefault(() => Option.some([])), + ), + customCursorModels: Schema.Array(Schema.String).pipe( + Schema.withConstructorDefault(() => Option.some([])), + ), customCodexModels: Schema.Array(Schema.String).pipe( Schema.withConstructorDefault(() => Option.some([])), ), @@ -73,6 +82,14 @@ export function normalizeCustomModelSlugs( return normalizedModels; } +function normalizeAppSettings(settings: AppSettings): AppSettings { + return { + ...settings, + customCodexModels: normalizeCustomModelSlugs(settings.customCodexModels, "codex"), + customClaudeModels: normalizeCustomModelSlugs(settings.customClaudeModels, "claudeCode"), + customCursorModels: normalizeCustomModelSlugs(settings.customCursorModels, "cursor"), + }; +} export function getAppModelOptions( provider: ProviderKind, customModels: readonly string[], @@ -151,10 +168,7 @@ export function useAppSettings() { const updateSettings = useCallback( (patch: Partial) => { - setSettings((prev) => ({ - ...prev, - ...patch, - })); + setSettings((prev) => normalizeAppSettings({ ...prev, ...patch })); }, [setSettings], ); diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 52637695e..5fd8b4109 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -54,6 +54,9 @@ import { deriveActiveWorkStartedAt, deriveActivePlanState, findLatestProposedPlan, + type PendingApproval, + type PendingUserInput, + PROVIDER_OPTIONS, deriveWorkLogEntries, hasToolActivityForTurn, isLatestTurnSettled, diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index 2af920527..e012990ba 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -246,7 +246,7 @@ function shouldRemoveDraft(draft: ComposerThreadDraftState): boolean { } function normalizeProviderKind(value: unknown): ProviderKind | null { - return value === "codex" ? value : null; + return value === "codex" || value === "claudeCode" || value === "cursor" ? value : null; } function revokeObjectPreviewUrl(previewUrl: string): void { diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index 3f18e4895..bdce981e6 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -74,6 +74,10 @@ function getCustomModelsForProvider( provider: ProviderKind, ) { switch (provider) { + case "claudeCode": + return settings.customClaudeModels; + case "cursor": + return settings.customCursorModels; case "codex": default: return settings.customCodexModels; @@ -85,6 +89,10 @@ function getDefaultCustomModelsForProvider( provider: ProviderKind, ) { switch (provider) { + case "claudeCode": + return defaults.customClaudeModels; + case "cursor": + return defaults.customCursorModels; case "codex": default: return defaults.customCodexModels; @@ -93,6 +101,10 @@ function getDefaultCustomModelsForProvider( function patchCustomModels(provider: ProviderKind, models: string[]) { switch (provider) { + case "claudeCode": + return { customClaudeModels: models }; + case "cursor": + return { customCursorModels: models }; case "codex": default: return { customCodexModels: models }; @@ -109,6 +121,8 @@ function SettingsRouteView() { Record >({ codex: "", + claudeCode: "", + cursor: "", }); const [customModelErrorByProvider, setCustomModelErrorByProvider] = useState< Partial> diff --git a/apps/web/src/store.test.ts b/apps/web/src/store.test.ts index 92d084f2d..5fcb258a6 100644 --- a/apps/web/src/store.test.ts +++ b/apps/web/src/store.test.ts @@ -191,7 +191,7 @@ describe("store pure functions", () => { }); describe("store read model sync", () => { - it("falls back to the codex default for unsupported provider models without an active session", () => { + it("preserves claude model slugs without an active session", () => { const initialState = makeState(makeThread()); const readModel = makeReadModel( makeReadModelThread({ @@ -201,7 +201,51 @@ describe("store read model sync", () => { const next = syncServerReadModel(initialState, readModel); - expect(next.threads[0]?.model).toBe(DEFAULT_MODEL_BY_PROVIDER.codex); + expect(next.threads[0]?.model).toBe("claude-opus-4-6"); + }); + + it("resolves claude aliases when session provider is claudeCode", () => { + const initialState = makeState(makeThread()); + const readModel = makeReadModel( + makeReadModelThread({ + model: "sonnet", + session: { + threadId: ThreadId.makeUnsafe("thread-1"), + status: "ready", + providerName: "claudeCode", + runtimeMode: "approval-required", + activeTurnId: null, + lastError: null, + updatedAt: "2026-02-27T00:00:00.000Z", + }, + }), + ); + + const next = syncServerReadModel(initialState, readModel); + + expect(next.threads[0]?.model).toBe("claude-sonnet-4-6"); + }); + + it("resolves cursor aliases when session provider is cursor", () => { + const initialState = makeState(makeThread()); + const readModel = makeReadModel( + makeReadModelThread({ + model: "composer", + session: { + threadId: ThreadId.makeUnsafe("thread-1"), + status: "ready", + providerName: "cursor", + runtimeMode: "approval-required", + activeTurnId: null, + lastError: null, + updatedAt: "2026-02-27T00:00:00.000Z", + }, + }), + ); + + const next = syncServerReadModel(initialState, readModel); + + expect(next.threads[0]?.model).toBe("composer-1.5"); }); it("preserves the current project order when syncing incoming read model updates", () => { diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts index faebe4b0f..dadd24b28 100644 --- a/apps/web/src/store.ts +++ b/apps/web/src/store.ts @@ -189,26 +189,54 @@ function toLegacySessionStatus( } function toLegacyProvider(providerName: string | null): ProviderKind { - if (providerName === "codex") { + if (providerName === "codex" || providerName === "claudeCode" || providerName === "cursor") { return providerName; } return "codex"; } const CODEX_MODEL_SLUGS = new Set(getModelOptions("codex").map((option) => option.slug)); +const CLAUDE_MODEL_SLUGS = new Set( + getModelOptions("claudeCode").map((option) => option.slug), +); +const CURSOR_MODEL_SLUGS = new Set(getModelOptions("cursor").map((option) => option.slug)); +const CURSOR_DISTINCT_MODEL_SLUGS = new Set( + [...CURSOR_MODEL_SLUGS].filter( + (slug) => !CODEX_MODEL_SLUGS.has(slug) && !CLAUDE_MODEL_SLUGS.has(slug), + ), +); function inferProviderForThreadModel(input: { readonly model: string; readonly sessionProviderName: string | null; }): ProviderKind { - if (input.sessionProviderName === "codex") { + if ( + input.sessionProviderName === "codex" || + input.sessionProviderName === "claudeCode" || + input.sessionProviderName === "cursor" + ) { return input.sessionProviderName; } + const normalizedCursor = normalizeModelSlug(input.model, "cursor"); + if (normalizedCursor && CURSOR_DISTINCT_MODEL_SLUGS.has(normalizedCursor)) { + return "cursor"; + } + const normalizedClaude = normalizeModelSlug(input.model, "claudeCode"); + if (normalizedClaude && CLAUDE_MODEL_SLUGS.has(normalizedClaude)) { + return "claudeCode"; + } const normalizedCodex = normalizeModelSlug(input.model, "codex"); if (normalizedCodex && CODEX_MODEL_SLUGS.has(normalizedCodex)) { return "codex"; } - return "codex"; + if ( + input.model.trim().startsWith("composer-") || + input.model.trim().startsWith("gemini-") || + input.model.trim().endsWith("-thinking") + ) { + return "cursor"; + } + return input.model.trim().startsWith("claude-") ? "claudeCode" : "codex"; } function resolveWsHttpOrigin(): string { diff --git a/packages/contracts/src/orchestration.test.ts b/packages/contracts/src/orchestration.test.ts index 25a641edb..f98bca262 100644 --- a/packages/contracts/src/orchestration.test.ts +++ b/packages/contracts/src/orchestration.test.ts @@ -130,11 +130,11 @@ it.effect("preserves explicit provider and runtime mode in thread.turn.start", ( text: "hello", attachments: [], }, - provider: "codex", + provider: "claudeCode", runtimeMode: "full-access", createdAt: "2026-01-01T00:00:00.000Z", }); - assert.strictEqual(parsed.provider, "codex"); + assert.strictEqual(parsed.provider, "claudeCode"); assert.strictEqual(parsed.runtimeMode, "full-access"); assert.strictEqual(parsed.interactionMode, DEFAULT_PROVIDER_INTERACTION_MODE); }), diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 17c5eb21d..5cb308a81 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -27,7 +27,7 @@ export const ORCHESTRATION_WS_CHANNELS = { domainEvent: "orchestration.domainEvent", } as const; -export const ProviderKind = Schema.Literal("codex"); +export const ProviderKind = Schema.Literals(["codex", "claudeCode", "cursor"]); export type ProviderKind = typeof ProviderKind.Type; export const ProviderApprovalPolicy = Schema.Literals([ "untrusted", @@ -49,6 +49,18 @@ const CodexProviderStartOptions = Schema.Struct({ }); const ProviderStartOptions = Schema.Struct({ codex: Schema.optional(CodexProviderStartOptions), + claudeCode: Schema.optional( + Schema.Struct({ + binaryPath: Schema.optional(TrimmedNonEmptyString), + permissionMode: Schema.optional(TrimmedNonEmptyString), + maxThinkingTokens: Schema.optional(NonNegativeInt), + }), + ), + cursor: Schema.optional( + Schema.Struct({ + binaryPath: Schema.optional(TrimmedNonEmptyString), + }), + ), }); export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]); export type RuntimeMode = typeof RuntimeMode.Type; diff --git a/packages/contracts/src/providerRuntime.test.ts b/packages/contracts/src/providerRuntime.test.ts index 8dad509ae..7f578c276 100644 --- a/packages/contracts/src/providerRuntime.test.ts +++ b/packages/contracts/src/providerRuntime.test.ts @@ -10,7 +10,7 @@ describe("ProviderRuntimeEvent", () => { const parsed = decodeRuntimeEvent({ type: "turn.plan.updated", eventId: "event-1", - provider: "codex", + provider: "claudeCode", sessionId: "runtime-session-1", createdAt: "2026-02-28T00:00:00.000Z", threadId: "thread-1", @@ -56,7 +56,7 @@ describe("ProviderRuntimeEvent", () => { const parsed = decodeRuntimeEvent({ type: "user-input.requested", eventId: "event-2", - provider: "codex", + provider: "claudeCode", sessionId: "runtime-session-2", createdAt: "2026-02-28T00:00:01.000Z", threadId: "thread-2", @@ -94,7 +94,7 @@ describe("ProviderRuntimeEvent", () => { const parsed = decodeRuntimeEvent({ type: "user-input.resolved", eventId: "event-3", - provider: "codex", + provider: "claudeCode", sessionId: "runtime-session-2", createdAt: "2026-02-28T00:00:02.000Z", threadId: "thread-2", diff --git a/packages/contracts/src/providerRuntime.ts b/packages/contracts/src/providerRuntime.ts index d76475ab5..d317ed4f1 100644 --- a/packages/contracts/src/providerRuntime.ts +++ b/packages/contracts/src/providerRuntime.ts @@ -19,6 +19,8 @@ const RuntimeEventRawSource = Schema.Literals([ "codex.app-server.notification", "codex.app-server.request", "codex.eventmsg", + "claude.sdk.message", + "claude.sdk.permission", "codex.sdk.thread-event", ]); export type RuntimeEventRawSource = typeof RuntimeEventRawSource.Type; From de7cfba26188a5f45c227a2302463f4c18ce8614 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Mar 2026 21:36:56 -0800 Subject: [PATCH 06/23] split: restore Claude SDK manifest and plan doc Co-authored-by: codex --- .plans/17-claude-code.md | 441 +++++++++++++++++++++++++++++++++++++++ apps/server/package.json | 1 + 2 files changed, 442 insertions(+) create mode 100644 .plans/17-claude-code.md diff --git a/.plans/17-claude-code.md b/.plans/17-claude-code.md new file mode 100644 index 000000000..822e978f5 --- /dev/null +++ b/.plans/17-claude-code.md @@ -0,0 +1,441 @@ +# Plan: Claude Code Integration (Orchestration Architecture) + +## Why this plan was rewritten + +The previous plan targeted a pre-orchestration architecture (`ProviderManager`, provider-native WS event methods, and direct provider UI wiring). The current app now routes everything through: + +1. `orchestration.dispatchCommand` (client intent) +2. `OrchestrationEngine` (decide + persist + publish domain events) +3. `ProviderCommandReactor` (domain intent -> `ProviderService`) +4. `ProviderService` (adapter routing + canonical runtime stream) +5. `ProviderRuntimeIngestion` (provider runtime -> internal orchestration commands) +6. `orchestration.domainEvent` (single push channel consumed by web) + +Claude integration must plug into this path instead of reintroducing legacy provider-specific flows. + +--- + +## Current constraints to design around (post-Stage 1) + +1. Provider runtime ingestion expects canonical `ProviderRuntimeEvent` shapes, not provider-native payloads. +2. Start input now uses typed `providerOptions` and generic `resumeCursor`; top-level provider-specific fields were removed. +3. `resumeCursor` is intentionally opaque outside adapters and must never be synthesized from `providerThreadId`. +4. `ProviderService` still requires adapter `startSession()` to return a `ProviderSession` with `threadId`. +5. Checkpoint revert currently calls `providerService.rollbackConversation()`, so Claude adapter needs a rollback strategy compatible with current reactor behavior. +6. Web currently marks Claude as unavailable (`"Claude Code (soon)"`) and model picker is Codex-only. + +--- + +## Architecture target + +Add Claude as a first-class provider adapter that emits canonical runtime events and works with existing orchestration reactors without adding new WS channels or bypass paths. + +Key decisions: + +1. Keep orchestration provider-agnostic; adapt Claude inside adapter/layer boundaries. +2. Use the existing canonical runtime stream (`ProviderRuntimeEvent`) as the only ingestion contract. +3. Keep provider session routing in `ProviderService` and `ProviderSessionDirectory`. +4. Add explicit provider selection to turn-start intent so first turn can start Claude session intentionally. + +--- + +## Phase 1: Contracts and command shape updates + +### 1.1 Provider-aware model contract + +Update `packages/contracts/src/model.ts` so model resolution can be provider-aware instead of Codex-only. + +Expected outcomes: + +1. Introduce provider-scoped model lists (Codex + Claude). +2. Add helpers that resolve model by provider. +3. Preserve backwards compatibility for existing Codex defaults. + +### 1.2 Turn-start provider intent + +Update `packages/contracts/src/orchestration.ts`: + +1. Add optional `provider: ProviderKind` to `ThreadTurnStartCommand`. +2. Carry provider through `ThreadTurnStartRequestedPayload`. +3. Keep existing command valid when provider is omitted. + +This removes the implicit “Codex unless session already exists” behavior as the only path. + +### 1.3 Provider session start input for Claude runtime knobs (completed) + +Update `packages/contracts/src/provider.ts`: + +1. Move provider-specific start fields into typed `providerOptions`: + - `providerOptions.codex` + - `providerOptions.claudeCode` +2. Keep `resumeCursor` as the single cross-provider resume input in `ProviderSessionStartInput`. +3. Deprecate/remove `resumeThreadId` from the generic start contract. +4. Treat `resumeCursor` as adapter-owned opaque state. + +### 1.4 Contract tests (completed) + +Update/add tests in `packages/contracts/src/*.test.ts` for: + +1. New command payload shape. +2. Provider-aware model resolution behavior. +3. Breaking-change expectations for removed top-level provider fields. + +--- + +## Phase 2: Claude adapter implementation + +### 2.1 Add adapter service + layer + +Create: + +1. `apps/server/src/provider/Services/ClaudeCodeAdapter.ts` +2. `apps/server/src/provider/Layers/ClaudeCodeAdapter.ts` + +Adapter must implement `ProviderAdapterShape`. + +### 2.1.a SDK dependency and baseline config + +Add server dependency: + +1. `@anthropic-ai/claude-agent-sdk` + +Baseline adapter options to support from day one: + +1. `cwd` +2. `model` +3. `pathToClaudeCodeExecutable` (from `providerOptions.claudeCode.binaryPath`) +4. `permissionMode` (from `providerOptions.claudeCode.permissionMode`) +5. `maxThinkingTokens` (from `providerOptions.claudeCode.maxThinkingTokens`) +6. `resume` +7. `resumeSessionAt` +8. `includePartialMessages` +9. `canUseTool` +10. `hooks` +11. `env` and `additionalDirectories` (if needed for sandbox/workspace parity) + +### 2.2 Claude runtime bridge + +Implement a Claude runtime bridge (either directly in adapter layer or via dedicated manager file) that wraps Agent SDK query lifecycle. + +Required capabilities: + +1. Long-lived session context per adapter session. +2. Multi-turn input queue. +3. Interrupt support. +4. Approval request/response bridge. +5. Resume support via opaque `resumeCursor` (parsed inside Claude adapter only). + +#### 2.2.a Agent SDK details to preserve + +The adapter should explicitly rely on these SDK capabilities: + +1. `query()` returns an async iterable message stream and control methods (`interrupt`, `setModel`, `setPermissionMode`, `setMaxThinkingTokens`, account/status helpers). +2. Multi-turn input is supported via async-iterable prompt input. +3. Tool approval decisions are provided via `canUseTool`. +4. Resume support uses `resume` and optional `resumeSessionAt`, both derived by parsing adapter-owned `resumeCursor`. +5. Hooks can be used for lifecycle signals (`Stop`, `PostToolUse`, etc.) when we need adapter-originated checkpoint/runtime events. + +#### 2.2.b Effect-native session lifecycle skeleton + +```ts +import { query } from "@anthropic-ai/claude-agent-sdk"; +import { Effect } from "effect"; + +const acquireSession = (input: ProviderSessionStartInput) => + Effect.acquireRelease( + Effect.tryPromise({ + try: async () => { + const claudeOptions = input.providerOptions?.claudeCode; + const resumeState = readClaudeResumeState(input.resumeCursor); + const abortController = new AbortController(); + const result = query({ + prompt: makePromptAsyncIterable(), + options: { + cwd: input.cwd, + model: input.model, + permissionMode: claudeOptions?.permissionMode, + maxThinkingTokens: claudeOptions?.maxThinkingTokens, + pathToClaudeCodeExecutable: claudeOptions?.binaryPath, + resume: resumeState?.threadId, + resumeSessionAt: resumeState?.sessionAt, + signal: abortController.signal, + includePartialMessages: true, + canUseTool: makeCanUseTool(), + hooks: makeClaudeHooks(), + }, + }); + return { abortController, result }; + }, + catch: (cause) => + new ProviderAdapterProcessError({ + provider: "claudeCode", + sessionId: "pending", + detail: "Failed to start Claude runtime session.", + cause, + }), + }), + ({ abortController }) => Effect.sync(() => abortController.abort()), + ); +``` + +#### 2.2.c AsyncIterable -> Effect Stream integration + +Preferred when available in the pinned Effect version: + +```ts +const sdkMessageStream = Stream.fromAsyncIterable( + session.result, + (cause) => + new ProviderAdapterProcessError({ + provider: "claudeCode", + sessionId, + detail: "Claude runtime stream failed.", + cause, + }), +); +``` + +Portable fallback (already aligned with current server patterns): + +```ts +const sdkMessageStream = Stream.async((emit) => { + let cancelled = false; + void (async () => { + try { + for await (const message of session.result) { + if (cancelled) break; + emit.single(message); + } + emit.end(); + } catch (cause) { + emit.fail( + new ProviderAdapterProcessError({ + provider: "claudeCode", + sessionId, + detail: "Claude runtime stream failed.", + cause, + }), + ); + } + })(); + return Effect.sync(() => { + cancelled = true; + }); +}); +``` + +### 2.3 Canonical event mapping + +Claude adapter must translate Agent SDK output into canonical `ProviderRuntimeEvent`. + +Initial mapping target: + +1. assistant text deltas -> `content.delta` +2. final assistant text -> `item.completed` and/or `turn.completed` +3. approval requests -> `request.opened` +4. approval results -> `request.resolved` +5. system lifecycle -> `session.*`, `thread.*`, `turn.*` +6. errors -> `runtime.error` +7. plan/proposed-plan content when derivable + +Implementation note: + +1. Keep raw Claude message on `raw` for debugging. +2. Prefer canonical item/request kinds over provider-native enums. +3. If Claude emits extra event kinds we do not model yet, map them to `tool.summary`, `runtime.warning`, or `unknown`-compatible payloads instead of dropping silently. + +### 2.4 Resume cursor strategy + +Define Claude-owned opaque resume state, e.g.: + +```ts +interface ClaudeResumeCursor { + readonly version: 1; + readonly threadId?: string; + readonly sessionAt?: string; +} +``` + +Rules: + +1. Serialize only adapter-owned state into `resumeCursor`. +2. Parse/validate only inside Claude adapter. +3. Store updated cursor when Claude runtime yields enough data to resume safely. +4. Never overload orchestration thread id as Claude thread id. + +### 2.5 Interrupt and stop semantics + +Map orchestration stop/interrupt expectations onto SDK controls: + +1. `interruptTurn()` -> active query interrupt. +2. `stopSession()` -> close session resources and prevent future sends. +3. `rollbackThread()` -> see Phase 4. + +--- + +## Phase 3: Provider service and composition + +### 3.1 Register Claude adapter + +Update provider registry layer to include Claude: + +1. add `claudeCode` -> `ClaudeCodeAdapter` +2. ensure `ProviderService.listProviderStatuses()` reports Claude availability + +### 3.2 Persist provider binding + +Current `ProviderSessionDirectory` already stores provider/thread binding and opaque `resumeCursor`. + +Required validation: + +1. Claude bindings survive restart. +2. resume cursor remains opaque and round-trips untouched. +3. stopAll + restart can recover Claude sessions when possible. + +### 3.3 Provider start routing + +Update `ProviderCommandReactor` / orchestration flow: + +1. If a thread turn start requests `provider: "claudeCode"`, start Claude if no active session exists. +2. If a thread already has Claude session binding, reuse it. +3. If provider switches between Codex and Claude, explicitly stop/rebind before next send. + +--- + +## Phase 4: Checkpoint and revert strategy + +Claude does not necessarily expose the same conversation rewind primitive as Codex app-server. Current architecture expects `providerService.rollbackConversation()`. + +Pick one explicit strategy: + +### Option A: provider-native rewind + +If SDK/runtime supports safe rewind: + +1. implement in Claude adapter +2. keep `CheckpointReactor` unchanged + +### Option B: session restart + state truncation shim + +If no native rewind exists: + +1. Claude adapter returns successful rollback by: + - stopping current Claude session + - clearing/rewriting stored Claude resume cursor to last safe resumable point + - forcing next turn to recreate session from persisted orchestration state +2. Document that rollback is “conversation reset to checkpoint boundary”, not provider-native turn deletion. + +Whichever option is chosen: + +1. behavior must be deterministic +2. checkpoint revert tests must pass under orchestration expectations +3. user-visible activity log should explain failures clearly when provider rollback is impossible + +--- + +## Phase 5: Web integration + +### 5.1 Provider picker and model picker + +Update web state/UI: + +1. allow choosing Claude as thread provider before first turn +2. show Claude model list from provider-aware model helpers +3. preserve existing Codex default behavior when provider omitted + +Likely touch points: + +1. `apps/web/src/store.ts` +2. `apps/web/src/components/ChatView.tsx` +3. `apps/web/src/types.ts` +4. `packages/shared/src/model.ts` + +### 5.2 Settings for Claude executable/options + +Add app settings if needed for: + +1. Claude binary path +2. default permission mode +3. default max thinking tokens + +Do not hardcode provider-specific config into generic session state if it belongs in app settings or typed `providerOptions`. + +### 5.3 Session rendering + +No new WS channel should be needed. Claude should appear through existing: + +1. thread messages +2. activities/worklog +3. approvals +4. session state +5. checkpoints/diffs + +--- + +## Phase 6: Testing strategy + +### 6.1 Contract tests + +Cover: + +1. provider-aware model schemas +2. provider field on turn-start command +3. provider-specific start options schema + +### 6.2 Adapter layer tests + +Add `ClaudeCodeAdapter.test.ts` covering: + +1. session start +2. event mapping +3. approval bridge +4. resume cursor parse/serialize +5. interrupt behavior +6. rollback behavior or explicit unsupported error path + +Use SDK-facing layer tests/mocks only at the boundary. Do not mock orchestration business logic in higher-level tests. + +### 6.3 Provider service integration tests + +Extend provider integration coverage so Claude is exercised through `ProviderService`: + +1. start Claude session +2. send turn +3. receive canonical runtime events +4. restart/recover using persisted binding + +### 6.4 Orchestration integration tests + +Add/extend integration tests around: + +1. first-turn provider selection +2. Claude approval requests routed through orchestration +3. Claude runtime ingestion -> messages/activities/session updates +4. checkpoint revert behavior under Claude +5. stopAll/restart recovery + +These should validate real orchestration flows, not just adapter behavior. + +--- + +## Phase 7: Rollout order + +Recommended implementation order: + +1. contracts/provider-aware models +2. provider field on turn-start +3. Claude adapter skeleton + start/send/stream +4. canonical event mapping +5. provider registry/service wiring +6. orchestration recovery + checkpoint strategy +7. web provider/model picker +8. full integration tests + +--- + +## Non-goals + +1. Reintroducing provider-specific WS methods/channels. +2. Storing provider-native thread ids as orchestration ids. +3. Bypassing orchestration engine for Claude-specific UI flows. +4. Encoding Claude resume semantics outside adapter-owned `resumeCursor`. diff --git a/apps/server/package.json b/apps/server/package.json index a6ffd53b8..7783cacc0 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -22,6 +22,7 @@ "test": "vitest run" }, "dependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.2.62", "@effect/platform-node": "catalog:", "@effect/sql-sqlite-bun": "catalog:", "@pierre/diffs": "^1.1.0-beta.16", From 01f01c5e6b9acfd90ca38ce5064c3fb9f49146f0 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Mar 2026 21:37:39 -0800 Subject: [PATCH 07/23] build: sync Claude SDK lockfile on sibling stack Co-authored-by: codex --- bun.lock | 143 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 72 insertions(+), 71 deletions(-) diff --git a/bun.lock b/bun.lock index b8e36149f..78ec9515c 100644 --- a/bun.lock +++ b/bun.lock @@ -48,6 +48,7 @@ "t3": "./dist/index.mjs", }, "dependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.2.62", "@effect/platform-node": "catalog:", "@effect/sql-sqlite-bun": "catalog:", "@pierre/diffs": "^1.1.0-beta.16", @@ -182,13 +183,15 @@ "vitest": "^4.0.0", }, "packages": { - "@astrojs/check": ["@astrojs/check@0.9.7", "", { "dependencies": { "@astrojs/language-server": "^2.16.1", "chokidar": "^4.0.3", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "bin/astro-check.js" } }, "sha512-dA7U5/OFg8/xaMUb2vUOOJuuJXnMpHy6F0BM8ZhL7WT5OkTBwJ0GoW38n4fC4CXt+lT9mLWL0y8Pa74tFByBpQ=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.76", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-HZxvnT8ZWkzCnQygaYCA0dl8RSUzuVbxE1YG4ecy6vh4nQbTT36CxUxBy+QVdR12pPQluncC0mCOLhI2918Eaw=="], - "@astrojs/compiler": ["@astrojs/compiler@3.0.0", "", {}, "sha512-MwAbDE5mawZ1SS+D8qWiHdprdME5Tlj2e0YjxnEICvcOpbSukNS7Sa7hA5PK+6RrmUr/t6Gi5YgrdZKjbO/WPQ=="], + "@astrojs/check": ["@astrojs/check@0.9.8", "", { "dependencies": { "@astrojs/language-server": "^2.16.5", "chokidar": "^4.0.3", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "bin/astro-check.js" } }, "sha512-LDng8446QLS5ToKjRHd3bgUdirvemVVExV7nRyJfW2wV36xuv7vDxwy5NWN9zqeSEDgg0Tv84sP+T3yEq+Zlkw=="], + + "@astrojs/compiler": ["@astrojs/compiler@3.0.1", "", {}, "sha512-z97oYbdebO5aoWzuJ/8q5hLK232+17KcLZ7cJ8BCWk6+qNzVxn/gftC0KzMBUTD8WAaBkPpNSQK6PXLnNrZ0CA=="], "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.8.0", "", { "dependencies": { "picomatch": "^4.0.3" } }, "sha512-J56GrhEiV+4dmrGLPNOl2pZjpHXAndWVyiVDYGDuw6MWKpBSEMLdFxHzeM/6sqaknw9M+HFfHZAcvi3OfT3D/w=="], - "@astrojs/language-server": ["@astrojs/language-server@2.16.4", "", { "dependencies": { "@astrojs/compiler": "^2.13.1", "@astrojs/yaml2ts": "^0.2.3", "@jridgewell/sourcemap-codec": "^1.5.5", "@volar/kit": "~2.4.28", "@volar/language-core": "~2.4.28", "@volar/language-server": "~2.4.28", "@volar/language-service": "~2.4.28", "muggle-string": "^0.4.1", "tinyglobby": "^0.2.15", "volar-service-css": "0.0.68", "volar-service-emmet": "0.0.68", "volar-service-html": "0.0.68", "volar-service-prettier": "0.0.68", "volar-service-typescript": "0.0.68", "volar-service-typescript-twoslash-queries": "0.0.68", "volar-service-yaml": "0.0.68", "vscode-html-languageservice": "^5.6.1", "vscode-uri": "^3.1.0" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "bin/nodeServer.js" } }, "sha512-42oqz9uX+hU1/rFniJvtYW9FbfZJ6syM2fYZFi7Ub71/kOvF1GSeMS8sA3Ogs3iOeNUWefk/ImwBiiHeNmJfSA=="], + "@astrojs/language-server": ["@astrojs/language-server@2.16.5", "", { "dependencies": { "@astrojs/compiler": "^2.13.1", "@astrojs/yaml2ts": "^0.2.3", "@jridgewell/sourcemap-codec": "^1.5.5", "@volar/kit": "~2.4.28", "@volar/language-core": "~2.4.28", "@volar/language-server": "~2.4.28", "@volar/language-service": "~2.4.28", "muggle-string": "^0.4.1", "tinyglobby": "^0.2.15", "volar-service-css": "0.0.70", "volar-service-emmet": "0.0.70", "volar-service-html": "0.0.70", "volar-service-prettier": "0.0.70", "volar-service-typescript": "0.0.70", "volar-service-typescript-twoslash-queries": "0.0.70", "volar-service-yaml": "0.0.70", "vscode-html-languageservice": "^5.6.2", "vscode-uri": "^3.1.0" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "bin/nodeServer.js" } }, "sha512-MEQvrbuiFDEo+LCO4vvYuTr3eZ4IluZ/n4BbUv77AWAJNEj/n0j7VqTvdL1rGloNTIKZTUd46p5RwYKsxQGY8w=="], "@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.0.0", "", { "dependencies": { "@astrojs/internal-helpers": "0.8.0", "@astrojs/prism": "4.0.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^4.0.0", "smol-toml": "^1.6.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-jTAXHPy45L7o1ljH4jYV+ShtOHtyQUa1mGp3a5fJp1soX8lInuTJQ6ihmldHzVM4Q7QptU4SzIDIcKbBJO7sXQ=="], @@ -548,45 +551,45 @@ "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/Zmj0yTYSvmha6TG1QnoLqVT7ZMRDqXvFXXBQpIjteEwx9qvUYMBH2xbiOFhDeMUJkGwC3D6fdKsFtaqUvkwNA=="], - "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.55.0", "", { "os": "android", "cpu": "arm" }, "sha512-NhvgAhncTSOhRahQSCnkK/4YIGPjTmhPurQQ2dwt2IvwCMTvZRW5vF2K10UBOxFve4GZDMw6LtXZdC2qeuYIVQ=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-IyfYPthZyiSKwAv/dLjeO18SaK8MxLI9Yss2JrRDyweQAkuL3LhEy7pwIwI7uA3KQc1Vdn20kdmj3q0oUIQL6A=="], - "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.55.0", "", { "os": "android", "cpu": "arm64" }, "sha512-P9iWRh+Ugqhg+D7rkc7boHX8o3H2h7YPcZHQIgvVBgnua5tk4LR2L+IBlreZs58/95cd2x3/004p5VsQM9z4SA=="], + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Ga5zYrzH6vc/VFxhn6MmyUnYEfy9vRpwTIks99mY3j6Nz30yYpIkWryI0QKPCgvGUtDSXVLEaMum5nA+WrNOSg=="], - "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.55.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-esakkJIt7WFAhT30P/Qzn96ehFpzdZ1mNuzpOb8SCW7lI4oB8VsyQnkSHREM671jfpuBb/o2ppzBCx5l0jpgMA=="], + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ogmbdJysnw/D4bDcpf1sPLpFThZ48lYp4aKYm10Z/6Nh1SON6NtnNhTNOlhEY296tDFItsZUz+2tgcSYqh8Eyw=="], - "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.55.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-xDMFRCCAEK9fOH6As2z8ELsC+VDGSFRHwIKVSilw+xhgLwTDFu37rtmRbmUlx8rRGS6cWKQPTc47AVxAZEVVPQ=="], + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-x8QE1h+RAtQ2g+3KPsP6Fk/tdz6zJQUv5c7fTrJxXV3GHOo+Ry5p/PsogU4U+iUZg0rj6hS+E4xi+mnwwlDCWQ=="], - "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.55.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mYZqnwUD7ALCRxGenyLd1uuG+rHCL+OTT6S8FcAbVm/ZT2AZMGjvibp3F6k1SKOb2aeqFATmwRykrE41Q0GWVw=="], + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6G+WMZvwJpMvY7my+/SHEjb7BTk/PFbePqLpmVmUJRIsJMy/UlyYqjpuh0RCgYYkPLcnXm1rUM04kbTk8yS1Yg=="], - "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.55.0", "", { "os": "linux", "cpu": "arm" }, "sha512-LcX6RYcF9vL9ESGwJW3yyIZ/d/ouzdOKXxCdey1q0XJOW1asrHsIg5MmyKdEBR4plQx+shvYeQne7AzW5f3T1w=="], + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-YYHBsk/sl7fYwQOok+6W5lBPeUEvisznV/HZD2IfZmF3Bns6cPC3Z0vCtSEOaAWTjYWN3jVsdu55jMxKlsdlhg=="], - "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.55.0", "", { "os": "linux", "cpu": "arm" }, "sha512-C+8GS1rPtK+dI7mJFkqoRBkDuqbrNihnyYQsJPS9ez+8zF9JzfvU19lawqt4l/Y23o5uQswE/DORa8aiXUih3w=="], + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+AZK8rOUr78y8WT6XkDb04IbMRqauNV+vgT6f8ZLOH8wnpQ9i7Nol0XLxAu+Cq7Sb+J9wC0j6Km5hG8rj47/yQ=="], - "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.55.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ErLE4XbmcCopA4/CIDiH6J1IAaDOMnf/KSx/aFObs4/OjAAM3sFKWGZ57pNOMxhhyBdcmcXwYymph9GwcpcqgQ=="], + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-urse2SnugwJRojUkGSSeH2LPMaje5Q50yQtvtL9HFckiyeqXzoFwOAZqD5TR29R2lq7UHidfFDM9EGcchcbb8A=="], - "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.55.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-/kp65avi6zZfqEng56TTuhiy3P/3pgklKIdf38yvYeJ9/PgEeRA2A2AqKAKbZBNAqUzrzHhz9jF6j/PZvhJzTQ=="], + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-rkTZkBfJ4TYLjansjSzL6mgZOdN5IvUnSq3oNJSLwBcNvy3dlgQtpHPrRxrCEbbcp7oQ6If0tkNaqfOsphYZ9g=="], - "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.55.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-A6pTdXwcEEwL/nmz0eUJ6WxmxcoIS+97GbH96gikAyre3s5deC7sts38ZVVowjS2QQFuSWkpA4ZmQC0jZSNvJQ=="], + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-uqL1kMH3u69/e1CH2EJhP3CP28jw2ExLsku4o8RVAZ7fySo9zOyI2fy9pVlTAp4voBLVgzndXi3SgtdyCTa2aA=="], - "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.55.0", "", { "os": "linux", "cpu": "none" }, "sha512-clj0lnIN+V52G9tdtZl0LbdTSurnZ1NZj92Je5X4lC7gP5jiCSW+Y/oiDiSauBAD4wrHt2S7nN3pA0zfKYK/6Q=="], + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-j0CcMBOgV6KsRaBdsebIeiy7hCjEvq2KdEsiULf2LZqAq0v1M1lWjelhCV57LxsqaIGChXFuFJ0RiFrSRHPhSg=="], - "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.55.0", "", { "os": "linux", "cpu": "none" }, "sha512-NNu08pllN5x/O94/sgR3DA8lbrGBnTHsINZZR0hcav1sj79ksTiKKm1mRzvZvacwQ0hUnGinFo+JO75ok2PxYg=="], + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-7VDOiL8cDG3DQ/CY3yKjbV1c4YPvc4vH8qW09Vv+5ukq3l/Kcyr6XGCd5NvxUmxqDb2vjMpM+eW/4JrEEsUetA=="], - "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.55.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-BvfQz3PRlWZRoEZ17dZCqgQsMRdpzGZomJkVATwCIGhHVVeHJMQdmdXPSjcT1DCNUrOjXnVyj1RGDj5+/Je2+Q=="], + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-JGRpX0M+ikD3WpwJ7vKcHKV6Kg0dT52BW2Eu2BupXotYeqGXBrbY+QPkAyKO6MNgKozyTNaRh3r7g+VWgyAQYQ=="], - "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.55.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ngSOoFCSBMKVQd24H8zkbcBNc7EHhjnF1sv3mC9NNXQ/4rRjI/4Dj9+9XoDZeFEkF1SX1COSBXF1b2Pr9rqdEw=="], + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-dNaICPvtmuxFP/VbqdofrLqdS3bM/AKJN3LMJD52si44ea7Be1cBk6NpfIahaysG9Uo+L98QKddU9CD5L8UHnQ=="], - "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.55.0", "", { "os": "linux", "cpu": "x64" }, "sha512-BDpP7W8GlaG7BR6QjGZAleYzxoyKc/D24spZIF2mB3XsfALQJJT/OBmP8YpeTb1rveFSBHzl8T7l0aqwkWNdGA=="], + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pF1vOtM+GuXmbklM1hV8WMsn6tCNPvkUzklj/Ej98JhlanbmA2RB1BILgOpwSuCTRTIYx2MXssmEyQQ90QF5aA=="], - "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.55.0", "", { "os": "none", "cpu": "arm64" }, "sha512-PS6GFvmde/pc3fCA2Srt51glr8Lcxhpf6WIBFfLphndjRrD34NEcses4TSxQrEcxYo6qVywGfylM0ZhSCF2gGA=="], + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-bp8NQ4RE6fDIFLa4bdBiOA+TAvkNkg+rslR+AvvjlLTYXLy9/uKAYLQudaQouWihLD/hgkrXIKKzXi5IXOewwg=="], - "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.55.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-P6JcLJGs/q1UOvDLzN8otd9JsH4tsuuPDv+p7aHqHM3PrKmYdmUvkNj4K327PTd35AYcznOCN+l4ZOaq76QzSw=="], + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-PxT4OJDfMOQBzo3OlzFb9gkoSD+n8qSBxyVq2wQSZIHFQYGEqIRTo9M0ZStvZm5fdhMqaVYpOnJvH2hUMEDk/g=="], - "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.55.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-gzkk4zE2zsE+WmRxFOiAZHpCpUNDFytEakqNXoNHW+PnYEOTPKDdW6nrzgSeTbGKVPXNAKQnRnMgrh7+n3Xueg=="], + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-PTRy6sIEPqy2x8PTP1baBNReN/BNEFmde0L+mYeHmjXE1Vlcc9+I5nsqENsB2yAm5wLkzPoTNCMY/7AnabT4/A=="], - "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.55.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZFALNow2/og75gvYzNP7qe+rREQ5xunktwA+lgykoozHZ6hw9bqg4fn5j2UvG4gIn1FXqrZHkOAXuPf5+GOYTQ=="], + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ=="], - "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.19", "", { "dependencies": { "@pierre/theme": "0.0.22", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-XxGPKkVW+1t2KJQfgjmSnS+93nI9+ACJl1XjhF3Lo4BdQJOxV3pHeyix31ySn/m/1llq6O/7bXucE0OYCK6Kog=="], + "@pierre/diffs": ["@pierre/diffs@1.1.0", "", { "dependencies": { "@pierre/theme": "0.0.22", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-wbxrzcmanJuHZb81iir09j42uU9AnKxXDtAuEQJbAnti5f2UfYdCQYejawuHZStFrlsMacCZLh/dDHmqvAaQCw=="], "@pierre/theme": ["@pierre/theme@0.0.22", "", {}, "sha512-ePUIdQRNGjrveELTU7fY89Xa7YGHHEy5Po5jQy/18lm32eRn96+tnYJEtFooGdffrx55KBUtOXfvVy/7LDFFhA=="], @@ -626,7 +629,7 @@ "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.3", "", { "os": "win32", "cpu": "x64" }, "sha512-a4VUQZH7LxGbUJ3qJ/TzQG8HxdHvf+jOnqf7B7oFx1TEBm+j2KNL2zr5SQ7wHkNAcaPevF6gf9tQnVBnC4mD+A=="], - "@rolldown/plugin-babel": ["@rolldown/plugin-babel@0.2.0", "", { "dependencies": { "picomatch": "^4.0.3" }, "peerDependencies": { "@babel/core": "^7.29.0 || ^8.0.0-rc.1", "@babel/plugin-transform-runtime": "^7.29.0 || ^8.0.0-rc.1", "@babel/runtime": "^7.27.0 || ^8.0.0-rc.1", "rolldown": "^1.0.0-rc.5", "vite": "^8.0.0" }, "optionalPeers": ["@babel/plugin-transform-runtime", "@babel/runtime", "vite"] }, "sha512-ZkajPtfcBTfLSZTmm40k2E0cwT5HDosBcj71fdUURXl7FfETb50jEbV39ujPw2RFp3ZpOugK3ATdX+HuFfWr9Q=="], + "@rolldown/plugin-babel": ["@rolldown/plugin-babel@0.2.1", "", { "dependencies": { "picomatch": "^4.0.3" }, "peerDependencies": { "@babel/core": "^7.29.0 || ^8.0.0-rc.1", "@babel/plugin-transform-runtime": "^7.29.0 || ^8.0.0-rc.1", "@babel/runtime": "^7.27.0 || ^8.0.0-rc.1", "rolldown": "^1.0.0-rc.5", "vite": "^8.0.0" }, "optionalPeers": ["@babel/plugin-transform-runtime", "@babel/runtime", "vite"] }, "sha512-pHDVHqFv26JNC8I500JZ0H4h1kvSyiE3V9gjEO9pRAgD1KrIdJvcHCokV6f7gG7Rx4vMOD11V8VUOpqdyGbKBw=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="], @@ -700,7 +703,7 @@ "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.3", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw=="], - "@tanstack/history": ["@tanstack/history@1.161.4", "", {}, "sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww=="], + "@tanstack/history": ["@tanstack/history@1.161.6", "", {}, "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg=="], "@tanstack/pacer": ["@tanstack/pacer@0.18.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/store": "^0.8.0" } }, "sha512-qhCRSFei0hokQr3xYcQXqxsRD/LKlgHCxHXtKHrQoImp4x2Zu6tUOpUGVH4y2qexIrzSu3aibQBNNfC3Eay6Mg=="], @@ -710,25 +713,25 @@ "@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="], - "@tanstack/react-router": ["@tanstack/react-router@1.166.7", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/react-store": "^0.9.1", "@tanstack/router-core": "1.166.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-LLcXu2nrCn2WL+w0YAbg3CRZIIO2cYVSC3y+ZYlFBxBs4hh8eoNP1EWFvRLZGCFYpqON7x6qUf1u0W7tH0cJJw=="], + "@tanstack/react-router": ["@tanstack/react-router@1.167.3", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.1", "@tanstack/router-core": "1.167.3", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-1qbSy4r+O7IBdmPLlcKsjB041Gq2MMnIEAYSGIjaMZIL4duUIQnOWLw4jTfjKil/IJz/9rO5JcvrbxOG5UTSdg=="], "@tanstack/react-store": ["@tanstack/react-store@0.8.1", "", { "dependencies": { "@tanstack/store": "0.8.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-XItJt+rG8c5Wn/2L/bnxys85rBpm0BfMbhb4zmPVLXAKY9POrp1xd6IbU4PKoOI+jSEGc3vntPRfLGSgXfE2Ig=="], - "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.22", "", { "dependencies": { "@tanstack/virtual-core": "3.13.22" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-EaOrBBJLi3M0bTMQRjGkxLXRw7Gizwntoy5E2Q2UnSbML7Mo2a1P/Hfkw5tw9FLzK62bj34Jl6VNbQfRV6eJcA=="], + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.23", "", { "dependencies": { "@tanstack/virtual-core": "3.13.23" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ=="], - "@tanstack/router-core": ["@tanstack/router-core@1.166.7", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/store": "^0.9.1", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-MCc8wYIIcxmbeidM8PL2QeaAjUIHyhEDIZPW6NGfn/uwvyi+K2ucn3AGCxxcXl4JGGm0Mx9+7buYl1v3HdcFrg=="], + "@tanstack/router-core": ["@tanstack/router-core@1.167.3", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/store": "^0.9.1", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-M/CxrTGKk1fsySJjd+Pzpbi3YLDz+cJSutDjSTMy12owWlOgHV/I6kzR0UxyaBlHraM6XgMHNA0XdgsS1fa4Nw=="], - "@tanstack/router-generator": ["@tanstack/router-generator@1.166.7", "", { "dependencies": { "@tanstack/router-core": "1.166.7", "@tanstack/router-utils": "1.161.4", "@tanstack/virtual-file-routes": "1.161.4", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-lBI0VS7J1zMrJhfvT+3FMq9jPdOrJ3VgciPXyYvZBF/a9Mr8T94MU78PqrBNuJbYh7qCFO14ZhArUFqkYGuozQ=="], + "@tanstack/router-generator": ["@tanstack/router-generator@1.166.11", "", { "dependencies": { "@tanstack/router-core": "1.167.3", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.6", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-Q/49wxURbft1oNOvo/eVAWZq/lNLK3nBGlavqhLToAYXY6LCzfMtRlE/y3XPHzYC9pZc09u5jvBR1k1E4hyGDQ=="], - "@tanstack/router-plugin": ["@tanstack/router-plugin@1.166.7", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.166.7", "@tanstack/router-generator": "1.166.7", "@tanstack/router-utils": "1.161.4", "@tanstack/virtual-file-routes": "1.161.4", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.166.7", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-R06qe5UwApb/u02wDITVxN++6QE4xsLFQCr029VZ+4V8gyIe35kr8UCg3Jiyl6D5GXxhj62U2Ei8jccdkQaivw=="], + "@tanstack/router-plugin": ["@tanstack/router-plugin@1.166.12", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.167.3", "@tanstack/router-generator": "1.166.11", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.6", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.167.3", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-PYsnN6goK6zBaVo63UVKjofv69+HHMKRQXymwN55JYKguNnNR8OZ6E12icPb0Olc5uIpPiGz1YI2+rbpmNKGHA=="], - "@tanstack/router-utils": ["@tanstack/router-utils@1.161.4", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-r8TpjyIZoqrXXaf2DDyjd44gjGBoyE+/oEaaH68yLI9ySPO1gUWmQENZ1MZnmBnpUGN24NOZxdjDLc8npK0SAw=="], + "@tanstack/router-utils": ["@tanstack/router-utils@1.161.6", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-nRcYw+w2OEgK6VfjirYvGyPLOK+tZQz1jkYcmH5AjMamQ9PycnlxZF2aEZtPpNoUsaceX2bHptn6Ub5hGXqNvw=="], "@tanstack/store": ["@tanstack/store@0.9.2", "", {}, "sha512-K013lUJEFJK2ofFQ/hZKJUmCnpcV00ebLyOyFOWQvyQHUOZp/iYO84BM6aOGiV81JzwbX0APTVmW8YI7yiG5oA=="], - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.22", "", {}, "sha512-isuUGKsc5TAPDoHSbWTbl1SCil54zOS2MiWz/9GCWHPUQOvNTQx8qJEWC7UWR0lShhbK0Lmkcf0SZYxvch7G3g=="], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.23", "", {}, "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg=="], - "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.4", "", {}, "sha512-42WoRePf8v690qG8yGRe/YOh+oHni9vUaUUfoqlS91U2scd3a5rkLtVsc6b7z60w3RogH0I00vdrC5AaeiZ18w=="], + "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.6", "", {}, "sha512-EGWs9yvJA821pUkwkiZLQW89CzUumHyJy8NKq229BubyoWXfDw1oWnTJYSS/hhbLiwP9+KpopjeF5wWwnCCyeQ=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], @@ -786,7 +789,7 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.0", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-Bu5/eP6td3WI654+tRq+ryW1PbgA90y5pqMKpb3U7UpNk6VjI53P/ncPUd192U9dSrepLy7DHnq1XEMDz5H++w=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], "@vitest/browser": ["@vitest/browser@4.1.0", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.0", "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.0.3", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.0" } }, "sha512-tG/iOrgbiHQks0ew7CdelUyNEHkv8NLrt+CqdTivIuoSnXvO7scWMn4Kqo78/UGY1NJ6Hv+vp8BvRnED/bjFdQ=="], @@ -852,7 +855,7 @@ "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], - "astro": ["astro@6.0.4", "", { "dependencies": { "@astrojs/compiler": "^3.0.0", "@astrojs/internal-helpers": "0.8.0", "@astrojs/markdown-remark": "7.0.0", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.0.1", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^1.1.1", "devalue": "^5.6.3", "diff": "^8.0.3", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.27.3", "flattie": "^1.1.1", "fontace": "~0.4.1", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.3", "rehype": "^13.0.2", "semver": "^7.7.4", "shiki": "^4.0.0", "smol-toml": "^1.6.0", "svgo": "^4.0.0", "tinyclip": "^0.1.6", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unist-util-visit": "^5.1.0", "unstorage": "^1.17.4", "vfile": "^6.0.3", "vite": "^7.3.1", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "bin/astro.mjs" } }, "sha512-1piLJCPTL/x7AMO2cjVFSTFyRqKuC3W8sSEySCt1aJio+p/wGs5H3K+Xr/rE9ftKtknLUtjxCqCE7/0NsXfGpQ=="], + "astro": ["astro@6.0.5", "", { "dependencies": { "@astrojs/compiler": "^3.0.0", "@astrojs/internal-helpers": "0.8.0", "@astrojs/markdown-remark": "7.0.0", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.0.1", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^1.1.1", "devalue": "^5.6.3", "diff": "^8.0.3", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.27.3", "flattie": "^1.1.1", "fontace": "~0.4.1", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.3", "rehype": "^13.0.2", "semver": "^7.7.4", "shiki": "^4.0.0", "smol-toml": "^1.6.0", "svgo": "^4.0.0", "tinyclip": "^0.1.6", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unist-util-visit": "^5.1.0", "unstorage": "^1.17.4", "vfile": "^6.0.3", "vite": "^7.3.1", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "bin/astro.mjs" } }, "sha512-JnLCwaoCaRXIHuIB8yNztJrd7M3hXrHUMAoQmeXtEBKxRu/738REhaCZ1lapjrS9HlpHsWTu3JUXTERB/0PA7g=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], @@ -866,7 +869,7 @@ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.7", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], @@ -896,7 +899,7 @@ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001778", "", {}, "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001779", "", {}, "sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -1156,7 +1159,7 @@ "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="], - "hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="], + "hookable": ["hookable@6.1.0", "", {}, "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw=="], "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], @@ -1282,7 +1285,7 @@ "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], - "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + "lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="], "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], @@ -1406,7 +1409,7 @@ "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], - "msw": ["msw@2.12.10", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw=="], + "msw": ["msw@2.12.11", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-dVg20zi2I2EvnwH/+WupzsOC2mCa7qsIhyMAWtfRikn6RKtwL9+7SaF1IQ5LyZry4tlUtf6KyTVhnlQiZXozTQ=="], "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], @@ -1450,7 +1453,7 @@ "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], - "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], + "oniguruma-to-es": ["oniguruma-to-es@4.3.5", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ=="], "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], @@ -1458,7 +1461,7 @@ "oxfmt": ["oxfmt@0.40.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.40.0", "@oxfmt/binding-android-arm64": "0.40.0", "@oxfmt/binding-darwin-arm64": "0.40.0", "@oxfmt/binding-darwin-x64": "0.40.0", "@oxfmt/binding-freebsd-x64": "0.40.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.40.0", "@oxfmt/binding-linux-arm-musleabihf": "0.40.0", "@oxfmt/binding-linux-arm64-gnu": "0.40.0", "@oxfmt/binding-linux-arm64-musl": "0.40.0", "@oxfmt/binding-linux-ppc64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-musl": "0.40.0", "@oxfmt/binding-linux-s390x-gnu": "0.40.0", "@oxfmt/binding-linux-x64-gnu": "0.40.0", "@oxfmt/binding-linux-x64-musl": "0.40.0", "@oxfmt/binding-openharmony-arm64": "0.40.0", "@oxfmt/binding-win32-arm64-msvc": "0.40.0", "@oxfmt/binding-win32-ia32-msvc": "0.40.0", "@oxfmt/binding-win32-x64-msvc": "0.40.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-g0C3I7xUj4b4DcagevM9kgH6+pUHytikxUcn3/VUkvzTNaaXBeyZqb7IBsHwojeXm4mTBEC/aBjBTMVUkZwWUQ=="], - "oxlint": ["oxlint@1.55.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.55.0", "@oxlint/binding-android-arm64": "1.55.0", "@oxlint/binding-darwin-arm64": "1.55.0", "@oxlint/binding-darwin-x64": "1.55.0", "@oxlint/binding-freebsd-x64": "1.55.0", "@oxlint/binding-linux-arm-gnueabihf": "1.55.0", "@oxlint/binding-linux-arm-musleabihf": "1.55.0", "@oxlint/binding-linux-arm64-gnu": "1.55.0", "@oxlint/binding-linux-arm64-musl": "1.55.0", "@oxlint/binding-linux-ppc64-gnu": "1.55.0", "@oxlint/binding-linux-riscv64-gnu": "1.55.0", "@oxlint/binding-linux-riscv64-musl": "1.55.0", "@oxlint/binding-linux-s390x-gnu": "1.55.0", "@oxlint/binding-linux-x64-gnu": "1.55.0", "@oxlint/binding-linux-x64-musl": "1.55.0", "@oxlint/binding-openharmony-arm64": "1.55.0", "@oxlint/binding-win32-arm64-msvc": "1.55.0", "@oxlint/binding-win32-ia32-msvc": "1.55.0", "@oxlint/binding-win32-x64-msvc": "1.55.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-T+FjepiyWpaZMhekqRpH8Z3I4vNM610p6w+Vjfqgj5TZUxHXl7N8N5IPvmOU8U4XdTRxqtNNTh9Y4hLtr7yvFg=="], + "oxlint": ["oxlint@1.56.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.56.0", "@oxlint/binding-android-arm64": "1.56.0", "@oxlint/binding-darwin-arm64": "1.56.0", "@oxlint/binding-darwin-x64": "1.56.0", "@oxlint/binding-freebsd-x64": "1.56.0", "@oxlint/binding-linux-arm-gnueabihf": "1.56.0", "@oxlint/binding-linux-arm-musleabihf": "1.56.0", "@oxlint/binding-linux-arm64-gnu": "1.56.0", "@oxlint/binding-linux-arm64-musl": "1.56.0", "@oxlint/binding-linux-ppc64-gnu": "1.56.0", "@oxlint/binding-linux-riscv64-gnu": "1.56.0", "@oxlint/binding-linux-riscv64-musl": "1.56.0", "@oxlint/binding-linux-s390x-gnu": "1.56.0", "@oxlint/binding-linux-x64-gnu": "1.56.0", "@oxlint/binding-linux-x64-musl": "1.56.0", "@oxlint/binding-openharmony-arm64": "1.56.0", "@oxlint/binding-win32-arm64-msvc": "1.56.0", "@oxlint/binding-win32-ia32-msvc": "1.56.0", "@oxlint/binding-win32-x64-msvc": "1.56.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-Q+5Mj5PVaH/R6/fhMMFzw4dT+KPB+kQW4kaL8FOIq7tfhlnEVp6+3lcWqFruuTNlUo9srZUW3qH7Id4pskeR6g=="], "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], @@ -1510,7 +1513,7 @@ "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], - "pure-rand": ["pure-rand@8.0.0", "", {}, "sha512-7rgWlxG2gAvFPIQfUreo1XYlNvrQ9VnQPFWdncPkdl3icucLK0InOxsaafbvxGTnI6Bk/Rxmslg0lQlRCuzOXw=="], + "pure-rand": ["pure-rand@8.1.0", "", {}, "sha512-53B3MB8wetRdD6JZ4W/0gDKaOvKwuXrEmV1auQc0hASWge8rieKV4PCCVNVbJ+i24miiubb4c/B+dg8Ho0ikYw=="], "quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], @@ -1676,7 +1679,7 @@ "tinyclip": ["tinyclip@0.1.12", "", {}, "sha512-Ae3OVUqifDw0wBriIBS7yVaW44Dp6eSHQcyq4Igc7eN2TJH/2YsicswaW+J/OuMvhpDPOKEgpAZCjkb4hpoyeA=="], - "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], @@ -1684,9 +1687,9 @@ "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], - "tldts": ["tldts@7.0.25", "", { "dependencies": { "tldts-core": "^7.0.25" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w=="], + "tldts": ["tldts@7.0.26", "", { "dependencies": { "tldts-core": "^7.0.26" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ=="], - "tldts-core": ["tldts-core@7.0.25", "", {}, "sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw=="], + "tldts-core": ["tldts-core@7.0.26", "", {}, "sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -1710,19 +1713,19 @@ "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], - "turbo": ["turbo@2.8.16", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.16", "turbo-darwin-arm64": "2.8.16", "turbo-linux-64": "2.8.16", "turbo-linux-arm64": "2.8.16", "turbo-windows-64": "2.8.16", "turbo-windows-arm64": "2.8.16" }, "bin": { "turbo": "bin/turbo" } }, "sha512-u6e9e3cTTpE2adQ1DYm3A3r8y3LAONEx1jYvJx6eIgSY4bMLxIxs0riWzI0Z/IK903ikiUzRPZ2c1Ph5lVLkhA=="], + "turbo": ["turbo@2.8.17", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.17", "turbo-darwin-arm64": "2.8.17", "turbo-linux-64": "2.8.17", "turbo-linux-arm64": "2.8.17", "turbo-windows-64": "2.8.17", "turbo-windows-arm64": "2.8.17" }, "bin": { "turbo": "bin/turbo" } }, "sha512-YwPsNSqU2f/RXU/+Kcb7cPkPZARxom4+me7LKEdN5jsvy2tpfze3zDZ4EiGrJnvOm9Avu9rK0aaYsP7qZ3iz7A=="], - "turbo-darwin-64": ["turbo-darwin-64@2.8.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-KWa4hUMWrpADC6Q/wIHRkBLw6X6MV9nx6X7hSXbTrrMz0KdaKhmfudUZ3sS76bJFmgArBU25cSc0AUyyrswYxg=="], + "turbo-darwin-64": ["turbo-darwin-64@2.8.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZFkv2hv7zHpAPEXBF6ouRRXshllOavYc+jjcrYyVHvxVTTwJWsBZwJ/gpPzmOKGvkSjsEyDO5V6aqqtZzwVF+Q=="], - "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NBgaqBDLQSZlJR4D5XCkQq6noaO0RvIgwm5eYFJYL3bH5dNu8o0UBpq7C5DYnQI8+ybyoHFjT5/icN4LeUYLow=="], + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5DXqhQUt24ycEryXDfMNKEkW5TBHs+QmU23a2qxXwwFDaJsWcPo2obEhBxxdEPOv7qmotjad+09RGeWCcJ9JDw=="], - "turbo-linux-64": ["turbo-linux-64@2.8.16", "", { "os": "linux", "cpu": "x64" }, "sha512-VYPdcCRevI9kR/hr1H1xwXy7QQt/jNKiim1e1mjANBXD2E9VZWMkIL74J1Huad5MbU3/jw7voHOqDPLJPC2p6w=="], + "turbo-linux-64": ["turbo-linux-64@2.8.17", "", { "os": "linux", "cpu": "x64" }, "sha512-KLUbz6w7F73D/Ihh51hVagrKR0/CTsPEbRkvXLXvoND014XJ4BCrQUqSxlQ4/hu+nqp1v5WlM85/h3ldeyujuA=="], - "turbo-linux-arm64": ["turbo-linux-arm64@2.8.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-beq8tgUVI3uwkQkXJMiOr/hfxQRw54M3elpBwqgYFfemiK5LhCjjcwO0DkE8GZZfElBIlk+saMAQOZy3885wNQ=="], + "turbo-linux-arm64": ["turbo-linux-arm64@2.8.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-pJK67XcNJH40lTAjFu7s/rUlobgVXyB3A3lDoq+/JccB3hf+SysmkpR4Itlc93s8LEaFAI4mamhFuTV17Z6wOg=="], - "turbo-windows-64": ["turbo-windows-64@2.8.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Ig7b46iUgiOIkea/D3Z7H+zNzvzSnIJcLYFpZLA0RxbUTrbLhv9qIPwv3pT9p/abmu0LXVKHxaOo+p26SuDhzw=="], + "turbo-windows-64": ["turbo-windows-64@2.8.17", "", { "os": "win32", "cpu": "x64" }, "sha512-EijeQ6zszDMmGZLP2vT2RXTs/GVi9rM0zv2/G4rNu2SSRSGFapgZdxgW4b5zUYLVaSkzmkpWlGfPfj76SW9yUg=="], - "turbo-windows-arm64": ["turbo-windows-arm64@2.8.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-fOWjbEA2PiE2HEnFQrwNZKYEdjewyPc2no9GmrXklZnTCuMsxeCN39aVlKpKpim03Zq/ykIuvApGwq8ZbfS2Yw=="], + "turbo-windows-arm64": ["turbo-windows-arm64@2.8.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-crpfeMPkfECd4V1PQ/hMoiyVcOy04+bWedu/if89S15WhOalHZ2BYUi6DOJhZrszY+mTT99OwpOsj4wNfb/GHQ=="], "type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="], @@ -1740,7 +1743,7 @@ "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], - "undici": ["undici@7.24.0", "", {}, "sha512-jxytwMHhsbdpBXxLAcuu0fzlQeXCNnWdDyRHpvWsUl8vd98UwYdl9YTyn8/HcpcJPC3pwUveefsa3zTxyD/ERg=="], + "undici": ["undici@7.24.4", "", {}, "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -1794,21 +1797,21 @@ "vitest": ["vitest@4.1.0", "", { "dependencies": { "@vitest/expect": "4.1.0", "@vitest/mocker": "4.1.0", "@vitest/pretty-format": "4.1.0", "@vitest/runner": "4.1.0", "@vitest/snapshot": "4.1.0", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.0", "@vitest/browser-preview": "4.1.0", "@vitest/browser-webdriverio": "4.1.0", "@vitest/ui": "4.1.0", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw=="], - "vitest-browser-react": ["vitest-browser-react@2.0.5", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "vitest": "^4.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YODQX8mHTJCyKNVYTWJrLEYrUtw+QfLl78owgvuE7C5ydgmGBq6v5s4jK2w6wdPhIZsN9PpV1rQbmAevWJjO9g=="], + "vitest-browser-react": ["vitest-browser-react@2.1.0", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "vitest": "^4.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/cOVQ+dZojhavfsbHjcfzB3zrUxG39HIbGdvK9vSBdGc8b8HRu5Bql0p8aXtKw4sb8/E8n5XEncQxvqHtfjjag=="], - "volar-service-css": ["volar-service-css@0.0.68", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-lJSMh6f3QzZ1tdLOZOzovLX0xzAadPhx8EKwraDLPxBndLCYfoTvnNuiFFV8FARrpAlW5C0WkH+TstPaCxr00Q=="], + "volar-service-css": ["volar-service-css@0.0.70", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-K1qyOvBpE3rzdAv3e4/6Rv5yizrYPy5R/ne3IWCAzLBuMO4qBMV3kSqWzj6KUVe6S0AnN6wxF7cRkiaKfYMYJw=="], - "volar-service-emmet": ["volar-service-emmet@0.0.68", "", { "dependencies": { "@emmetio/css-parser": "^0.4.1", "@emmetio/html-matcher": "^1.3.0", "@vscode/emmet-helper": "^2.9.3", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-nHvixrRQ83EzkQ4G/jFxu9Y4eSsXS/X2cltEPDM+K9qZmIv+Ey1w0tg1+6caSe8TU5Hgw4oSTwNMf/6cQb3LzQ=="], + "volar-service-emmet": ["volar-service-emmet@0.0.70", "", { "dependencies": { "@emmetio/css-parser": "^0.4.1", "@emmetio/html-matcher": "^1.3.0", "@vscode/emmet-helper": "^2.9.3", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-xi5bC4m/VyE3zy/n2CXspKeDZs3qA41tHLTw275/7dNWM/RqE2z3BnDICQybHIVp/6G1iOQj5c1qXMgQC08TNg=="], - "volar-service-html": ["volar-service-html@0.0.68", "", { "dependencies": { "vscode-html-languageservice": "^5.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-fru9gsLJxy33xAltXOh4TEdi312HP80hpuKhpYQD4O5hDnkNPEBdcQkpB+gcX0oK0VxRv1UOzcGQEUzWCVHLfA=="], + "volar-service-html": ["volar-service-html@0.0.70", "", { "dependencies": { "vscode-html-languageservice": "^5.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-eR6vCgMdmYAo4n+gcT7DSyBQbwB8S3HZZvSagTf0sxNaD4WppMCFfpqWnkrlGStPKMZvMiejRRVmqsX9dYcTvQ=="], - "volar-service-prettier": ["volar-service-prettier@0.0.68", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0", "prettier": "^2.2 || ^3.0" }, "optionalPeers": ["@volar/language-service", "prettier"] }, "sha512-grUmWHkHlebMOd6V8vXs2eNQUw/bJGJMjekh/EPf/p2ZNTK0Uyz7hoBRngcvGfJHMsSXZH8w/dZTForIW/4ihw=="], + "volar-service-prettier": ["volar-service-prettier@0.0.70", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0", "prettier": "^2.2 || ^3.0" }, "optionalPeers": ["@volar/language-service", "prettier"] }, "sha512-Z6BCFSpGVCd8BPAsZ785Kce1BGlWd5ODqmqZGVuB14MJvrR4+CYz6cDy4F+igmE1gMifqfvMhdgT8Aud4M5ngg=="], - "volar-service-typescript": ["volar-service-typescript@0.0.68", "", { "dependencies": { "path-browserify": "^1.0.1", "semver": "^7.6.2", "typescript-auto-import-cache": "^0.3.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-nls": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-z7B/7CnJ0+TWWFp/gh2r5/QwMObHNDiQiv4C9pTBNI2Wxuwymd4bjEORzrJ/hJ5Yd5+OzeYK+nFCKevoGEEeKw=="], + "volar-service-typescript": ["volar-service-typescript@0.0.70", "", { "dependencies": { "path-browserify": "^1.0.1", "semver": "^7.6.2", "typescript-auto-import-cache": "^0.3.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-nls": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-l46Bx4cokkUedTd74ojO5H/zqHZJ8SUuyZ0IB8JN4jfRqUM3bQFBHoOwlZCyZmOeO0A3RQNkMnFclxO4c++gsg=="], - "volar-service-typescript-twoslash-queries": ["volar-service-typescript-twoslash-queries@0.0.68", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-NugzXcM0iwuZFLCJg47vI93su5YhTIweQuLmZxvz5ZPTaman16JCvmDZexx2rd5T/75SNuvvZmrTOTNYUsfe5w=="], + "volar-service-typescript-twoslash-queries": ["volar-service-typescript-twoslash-queries@0.0.70", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-IdD13Z9N2Bu8EM6CM0fDV1E69olEYGHDU25X51YXmq8Y0CmJ2LNj6gOiBJgpS5JGUqFzECVhMNBW7R0sPdRTMQ=="], - "volar-service-yaml": ["volar-service-yaml@0.0.68", "", { "dependencies": { "vscode-uri": "^3.0.8", "yaml-language-server": "~1.19.2" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-84XgE02LV0OvTcwfqhcSwVg4of3MLNUWPMArO6Aj8YXqyEVnPu8xTEMY2btKSq37mVAPuaEVASI4e3ptObmqcA=="], + "volar-service-yaml": ["volar-service-yaml@0.0.70", "", { "dependencies": { "vscode-uri": "^3.0.8", "yaml-language-server": "~1.20.0" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-0c8bXDBeoATF9F6iPIlOuYTuZAC4c+yi0siQo920u7eiBJk8oQmUmg9cDUbR4+Gl++bvGP4plj3fErbJuPqdcQ=="], "vscode-css-languageservice": ["vscode-css-languageservice@6.3.10", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA=="], @@ -1856,7 +1859,7 @@ "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], - "yaml-language-server": ["yaml-language-server@1.19.2", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "lodash": "4.17.21", "prettier": "^3.5.0", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-uri": "^3.0.2", "yaml": "2.7.1" }, "bin": { "yaml-language-server": "bin/yaml-language-server" } }, "sha512-9F3myNmJzUN/679jycdMxqtydPSDRAarSj3wPiF7pchEPnO9Dg07Oc+gIYLqXR4L+g+FSEVXXv2+mr54StLFOg=="], + "yaml-language-server": ["yaml-language-server@1.20.0", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "prettier": "^3.5.0", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-uri": "^3.0.2", "yaml": "2.7.1" }, "bin": { "yaml-language-server": "bin/yaml-language-server" } }, "sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA=="], "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -1864,7 +1867,7 @@ "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], - "yjs": ["yjs@13.6.29", "", { "dependencies": { "lib0": "^0.2.99" } }, "sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ=="], + "yjs": ["yjs@13.6.30", "", { "dependencies": { "lib0": "^0.2.99" } }, "sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ=="], "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], @@ -1872,7 +1875,7 @@ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "zustand": ["zustand@5.0.11", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="], + "zustand": ["zustand@5.0.12", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], @@ -1898,13 +1901,13 @@ "@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - "@effect/platform-node/effect": ["effect@4.0.0-beta.31", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-w3QwJnlaLtWWiUSzhCXUTIisnULPsxLzpO6uqaBFjXybKx6FvCqsLJT6v4dV7G9eA9jeTtG6Gv7kF+jGe3HxzA=="], + "@effect/platform-node/effect": ["effect@4.0.0-beta.33", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-ln9emWPd1SemokSdOV43r2CbH1j8GTe9qbPvttmh9/j2OR0WNmj7UpjbN34llQgF9QV4IdcN6QdV2w8G7B7RyQ=="], - "@effect/platform-node-shared/effect": ["effect@4.0.0-beta.31", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-w3QwJnlaLtWWiUSzhCXUTIisnULPsxLzpO6uqaBFjXybKx6FvCqsLJT6v4dV7G9eA9jeTtG6Gv7kF+jGe3HxzA=="], + "@effect/platform-node-shared/effect": ["effect@4.0.0-beta.33", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-ln9emWPd1SemokSdOV43r2CbH1j8GTe9qbPvttmh9/j2OR0WNmj7UpjbN34llQgF9QV4IdcN6QdV2w8G7B7RyQ=="], - "@effect/sql-sqlite-bun/effect": ["effect@4.0.0-beta.31", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-w3QwJnlaLtWWiUSzhCXUTIisnULPsxLzpO6uqaBFjXybKx6FvCqsLJT6v4dV7G9eA9jeTtG6Gv7kF+jGe3HxzA=="], + "@effect/sql-sqlite-bun/effect": ["effect@4.0.0-beta.33", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-ln9emWPd1SemokSdOV43r2CbH1j8GTe9qbPvttmh9/j2OR0WNmj7UpjbN34llQgF9QV4IdcN6QdV2w8G7B7RyQ=="], - "@effect/vitest/effect": ["effect@4.0.0-beta.31", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-w3QwJnlaLtWWiUSzhCXUTIisnULPsxLzpO6uqaBFjXybKx6FvCqsLJT6v4dV7G9eA9jeTtG6Gv7kF+jGe3HxzA=="], + "@effect/vitest/effect": ["effect@4.0.0-beta.33", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-ln9emWPd1SemokSdOV43r2CbH1j8GTe9qbPvttmh9/j2OR0WNmj7UpjbN34llQgF9QV4IdcN6QdV2w8G7B7RyQ=="], "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], @@ -1988,8 +1991,6 @@ "vscode-json-languageservice/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], - "yaml-language-server/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - "yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="], "yaml-language-server/yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], From 99cd1c560ef6204ffa20f292925e5b8485aa9849 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 16 Mar 2026 10:24:55 -0700 Subject: [PATCH 08/23] Fix post-rebase provider integration Co-authored-by: codex --- .../orchestrationEngine.integration.test.ts | 26 +++++++++---------- .../provider/Layers/ClaudeCodeAdapter.test.ts | 26 ++++++++++++++----- .../src/provider/Layers/CodexAdapter.test.ts | 1 + .../provider/Layers/ProviderService.test.ts | 1 + apps/web/src/appSettings.ts | 5 ++-- apps/web/src/components/ChatView.logic.ts | 4 +++ apps/web/src/components/ChatView.tsx | 24 ++++++++++++----- 7 files changed, 57 insertions(+), 30 deletions(-) diff --git a/apps/server/integration/orchestrationEngine.integration.test.ts b/apps/server/integration/orchestrationEngine.integration.test.ts index ab155b91d..0bc306fc0 100644 --- a/apps/server/integration/orchestrationEngine.integration.test.ts +++ b/apps/server/integration/orchestrationEngine.integration.test.ts @@ -889,7 +889,7 @@ it.live("starts a claudeCode session on first turn when provider is requested", Effect.gen(function* () { yield* seedProjectAndThread(harness); - yield* harness.adapterHarness.queueTurnResponseForNextSession({ + yield* harness.adapterHarness!.queueTurnResponseForNextSession({ events: [ { type: "turn.started", @@ -943,7 +943,7 @@ it.live("recovers claudeCode sessions after provider stopAll using persisted res Effect.gen(function* () { yield* seedProjectAndThread(harness); - yield* harness.adapterHarness.queueTurnResponseForNextSession({ + yield* harness.adapterHarness!.queueTurnResponseForNextSession({ events: [ { type: "turn.started", @@ -981,14 +981,14 @@ it.live("recovers claudeCode sessions after provider stopAll using persisted res (entry) => entry.latestTurn?.turnId === "turn-1" && entry.session?.threadId === "thread-1", ); - yield* harness.providerService.stopAll(); + yield* harness.adapterHarness!.adapter.stopAll(); yield* waitForSync( - () => harness.adapterHarness.listActiveSessionIds(), + () => harness.adapterHarness!.listActiveSessionIds(), (sessionIds) => sessionIds.length === 0, "provider stopAll", ); - yield* harness.adapterHarness.queueTurnResponseForNextSession({ + yield* harness.adapterHarness!.queueTurnResponseForNextSession({ events: [ { type: "turn.started", @@ -1020,7 +1020,7 @@ it.live("recovers claudeCode sessions after provider stopAll using persisted res text: "After restart", }); yield* waitForSync( - () => harness.adapterHarness.getStartCount(), + () => harness.adapterHarness!.getStartCount(), (count) => count === 2, "claude provider recovery start", ); @@ -1047,7 +1047,7 @@ it.live("forwards claudeCode approval responses to the provider session", () => Effect.gen(function* () { yield* seedProjectAndThread(harness); - yield* harness.adapterHarness.queueTurnResponseForNextSession({ + yield* harness.adapterHarness!.queueTurnResponseForNextSession({ events: [ { type: "turn.started", @@ -1102,7 +1102,7 @@ it.live("forwards claudeCode approval responses to the provider session", () => ); const approvalResponses = yield* waitForSync( - () => harness.adapterHarness.getApprovalResponses(THREAD_ID), + () => harness.adapterHarness!.getApprovalResponses(THREAD_ID), (responses) => responses.length === 1, "claude provider approval response", ); @@ -1118,7 +1118,7 @@ it.live("forwards thread.turn.interrupt to claudeCode provider sessions", () => Effect.gen(function* () { yield* seedProjectAndThread(harness); - yield* harness.adapterHarness.queueTurnResponseForNextSession({ + yield* harness.adapterHarness!.queueTurnResponseForNextSession({ events: [ { type: "turn.started", @@ -1166,7 +1166,7 @@ it.live("forwards thread.turn.interrupt to claudeCode provider sessions", () => yield* harness.waitForDomainEvent((event) => event.type === "thread.turn-interrupt-requested"); const interruptCalls = yield* waitForSync( - () => harness.adapterHarness.getInterruptCalls(THREAD_ID), + () => harness.adapterHarness!.getInterruptCalls(THREAD_ID), (calls) => calls.length === 1, "claude provider interrupt call", ); @@ -1182,7 +1182,7 @@ it.live("reverts claudeCode turns and rolls back provider conversation state", ( Effect.gen(function* () { yield* seedProjectAndThread(harness); - yield* harness.adapterHarness.queueTurnResponseForNextSession({ + yield* harness.adapterHarness!.queueTurnResponseForNextSession({ events: [ { type: "turn.started", @@ -1224,7 +1224,7 @@ it.live("reverts claudeCode turns and rolls back provider conversation state", ( (entry) => entry.latestTurn?.turnId === "turn-1" && entry.session?.threadId === "thread-1", ); - yield* harness.adapterHarness.queueTurnResponse(THREAD_ID, { + yield* harness.adapterHarness!.queueTurnResponse(THREAD_ID, { events: [ { type: "turn.started", @@ -1290,7 +1290,7 @@ it.live("reverts claudeCode turns and rolls back provider conversation state", ( gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 2)), false, ); - assert.deepEqual(harness.adapterHarness.getRollbackCalls(THREAD_ID), [1]); + assert.deepEqual(harness.adapterHarness!.getRollbackCalls(THREAD_ID), [1]); }), "claudeCode", ), diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts index cd55c3983..6d5a8e29f 100644 --- a/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts @@ -546,14 +546,14 @@ describe("ClaudeCodeAdapterLive", () => { provider: "claudeCode", runtimeMode: "full-access", }); - assert.equal(session.threadId, undefined); + assert.equal(session.threadId, THREAD_ID); const turn = yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [], }); - assert.equal(turn.threadId, undefined); + assert.equal(turn.threadId, THREAD_ID); harness.query.emit({ type: "stream_event", @@ -592,13 +592,16 @@ describe("ClaudeCodeAdapterLive", () => { const sessionStarted = runtimeEvents[0]; assert.equal(sessionStarted?.type, "session.started"); if (sessionStarted?.type === "session.started") { - assert.equal("threadId" in sessionStarted, false); + assert.equal(sessionStarted.threadId, THREAD_ID); } const threadStarted = runtimeEvents[4]; assert.equal(threadStarted?.type, "thread.started"); if (threadStarted?.type === "thread.started") { - assert.equal(threadStarted.threadId, "sdk-thread-real"); + assert.equal(threadStarted.threadId, THREAD_ID); + assert.deepEqual(threadStarted.payload, { + providerThreadId: "sdk-thread-real", + }); } }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), @@ -700,9 +703,9 @@ describe("ClaudeCodeAdapterLive", () => { runtimeMode: "full-access", }); - assert.equal(session.threadId, "resume-thread-1"); + assert.equal(session.threadId, RESUME_THREAD_ID); assert.deepEqual(session.resumeCursor, { - threadId: "resume-thread-1", + threadId: RESUME_THREAD_ID, resume: "550e8400-e29b-41d4-a716-446655440000", resumeSessionAt: "assistant-99", turnCount: 3, @@ -911,7 +914,16 @@ describe("ClaudeCodeAdapterLive", () => { assert.equal(nativeEvents.length > 0, true); assert.equal(nativeEvents.some((record) => record.event?.provider === "claudeCode"), true); - assert.equal(nativeEvents.some((record) => String(record.event?.threadId) === String(session.threadId)), true); + assert.equal( + nativeEvents.some( + (record) => + String( + (record.event as { readonly providerThreadId?: string } | undefined) + ?.providerThreadId, + ) === "sdk-session-native-log", + ), + true, + ); assert.equal( nativeEvents.some((record) => String(record.event?.turnId) === String(turn.turnId)), true, diff --git a/apps/server/src/provider/Layers/CodexAdapter.test.ts b/apps/server/src/provider/Layers/CodexAdapter.test.ts index bc299fbc8..8bce7e927 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.test.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.test.ts @@ -22,6 +22,7 @@ import { type CodexAppServerSendTurnInput, } from "../../codexAppServerManager.ts"; import { ServerConfig } from "../../config.ts"; +import { ProviderAdapterValidationError } from "../Errors.ts"; import { CodexAdapter } from "../Services/CodexAdapter.ts"; import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; import { makeCodexAdapterLive } from "./CodexAdapter.ts"; diff --git a/apps/server/src/provider/Layers/ProviderService.test.ts b/apps/server/src/provider/Layers/ProviderService.test.ts index 10b3c9558..a2b2348f2 100644 --- a/apps/server/src/provider/Layers/ProviderService.test.ts +++ b/apps/server/src/provider/Layers/ProviderService.test.ts @@ -616,6 +616,7 @@ routing.layer("ProviderServiceLive routing", (it) => { }); yield* routing.codex.stopAll(); + yield* routing.claude.stopAll(); const remaining = yield* provider.listSessions(); assert.equal(remaining.length, 0); diff --git a/apps/web/src/appSettings.ts b/apps/web/src/appSettings.ts index 5157addd5..d28511b5b 100644 --- a/apps/web/src/appSettings.ts +++ b/apps/web/src/appSettings.ts @@ -34,13 +34,12 @@ const AppSettingsSchema = Schema.Struct({ Schema.withConstructorDefault(() => Option.some(DEFAULT_TIMESTAMP_FORMAT)), ), customCodexModels: Schema.Array(Schema.String).pipe( - customClaudeModels: Schema.Array(Schema.String).pipe( Schema.withConstructorDefault(() => Option.some([])), ), - customCursorModels: Schema.Array(Schema.String).pipe( + customClaudeModels: Schema.Array(Schema.String).pipe( Schema.withConstructorDefault(() => Option.some([])), ), - customCodexModels: Schema.Array(Schema.String).pipe( + customCursorModels: Schema.Array(Schema.String).pipe( Schema.withConstructorDefault(() => Option.some([])), ), }); diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 59e290431..114da4b69 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -118,8 +118,12 @@ export function cloneComposerImageForRetry( export function getCustomModelOptionsByProvider(settings: { customCodexModels: readonly string[]; + customClaudeModels: readonly string[]; + customCursorModels: readonly string[]; }): Record> { return { codex: getAppModelOptions("codex", settings.customCodexModels), + claudeCode: getAppModelOptions("claudeCode", settings.customClaudeModels), + cursor: getAppModelOptions("cursor", settings.customCursorModels), }; } diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 5fd8b4109..7b0111e25 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -54,9 +54,6 @@ import { deriveActiveWorkStartedAt, deriveActivePlanState, findLatestProposedPlan, - type PendingApproval, - type PendingUserInput, - PROVIDER_OPTIONS, deriveWorkLogEntries, hasToolActivityForTurn, isLatestTurnSettled, @@ -503,7 +500,15 @@ export default function ChatView({ threadId }: ChatViewProps) { selectedProvider, activeThread?.model ?? activeProject?.model ?? getDefaultModel(selectedProvider), ); - const customModelsForSelectedProvider = settings.customCodexModels; + const customModelsByProvider = useMemo( + () => ({ + codex: settings.customCodexModels, + claudeCode: settings.customClaudeModels, + cursor: settings.customCursorModels, + }), + [settings.customClaudeModels, settings.customCodexModels, settings.customCursorModels], + ); + const customModelsForSelectedProvider = customModelsByProvider[selectedProvider]; const selectedModel = useMemo(() => { const draftModel = composerDraft.model; if (!draftModel) { @@ -514,7 +519,12 @@ export default function ChatView({ threadId }: ChatViewProps) { customModelsForSelectedProvider, draftModel, ) as ModelSlug; - }, [baseThreadModel, composerDraft.model, customModelsForSelectedProvider, selectedProvider]); + }, [ + baseThreadModel, + composerDraft.model, + customModelsForSelectedProvider, + selectedProvider, + ]); const reasoningOptions = getReasoningEffortOptions(selectedProvider); const supportsReasoningEffort = reasoningOptions.length > 0; const selectedEffort = composerDraft.effort ?? getDefaultReasoningEffort(selectedProvider); @@ -2885,17 +2895,17 @@ export default function ChatView({ threadId }: ChatViewProps) { setComposerDraftProvider(activeThread.id, provider); setComposerDraftModel( activeThread.id, - resolveAppModelSelection(provider, settings.customCodexModels, model), + resolveAppModelSelection(provider, customModelsByProvider[provider], model), ); scheduleComposerFocus(); }, [ activeThread, + customModelsByProvider, lockedProvider, scheduleComposerFocus, setComposerDraftModel, setComposerDraftProvider, - settings.customCodexModels, ], ); const onEffortSelect = useCallback( From ff34d2c708e00945ae542e1484739754dd4e9286 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 16 Mar 2026 10:28:24 -0700 Subject: [PATCH 09/23] lock --- apps/server/package.json | 2 +- bun.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/package.json b/apps/server/package.json index 7783cacc0..f8f3ab899 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -22,7 +22,7 @@ "test": "vitest run" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.2.62", + "@anthropic-ai/claude-agent-sdk": "^0.2.76", "@effect/platform-node": "catalog:", "@effect/sql-sqlite-bun": "catalog:", "@pierre/diffs": "^1.1.0-beta.16", diff --git a/bun.lock b/bun.lock index 78ec9515c..14eece2d6 100644 --- a/bun.lock +++ b/bun.lock @@ -48,7 +48,7 @@ "t3": "./dist/index.mjs", }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.2.62", + "@anthropic-ai/claude-agent-sdk": "^0.2.76", "@effect/platform-node": "catalog:", "@effect/sql-sqlite-bun": "catalog:", "@pierre/diffs": "^1.1.0-beta.16", From 858116d83695493c12f4b67792ff3ed208e57cb5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 16 Mar 2026 10:28:36 -0700 Subject: [PATCH 10/23] fmt --- .../orchestrationEngine.integration.test.ts | 10 +- .../provider/Layers/ClaudeCodeAdapter.test.ts | 323 +++++++++--------- .../src/provider/Layers/ClaudeCodeAdapter.ts | 98 +++--- .../provider/Layers/ProviderService.test.ts | 2 +- .../provider/Services/ClaudeCodeAdapter.ts | 1 - apps/web/src/components/ChatView.tsx | 7 +- packages/shared/src/model.test.ts | 28 +- 7 files changed, 232 insertions(+), 237 deletions(-) diff --git a/apps/server/integration/orchestrationEngine.integration.test.ts b/apps/server/integration/orchestrationEngine.integration.test.ts index 0bc306fc0..44407af89 100644 --- a/apps/server/integration/orchestrationEngine.integration.test.ts +++ b/apps/server/integration/orchestrationEngine.integration.test.ts @@ -978,7 +978,8 @@ it.live("recovers claudeCode sessions after provider stopAll using persisted res yield* harness.waitForThread( THREAD_ID, - (entry) => entry.latestTurn?.turnId === "turn-1" && entry.session?.threadId === "thread-1", + (entry) => + entry.latestTurn?.turnId === "turn-1" && entry.session?.threadId === "thread-1", ); yield* harness.adapterHarness!.adapter.stopAll(); @@ -1163,7 +1164,9 @@ it.live("forwards thread.turn.interrupt to claudeCode provider sessions", () => threadId: THREAD_ID, createdAt: nowIso(), }); - yield* harness.waitForDomainEvent((event) => event.type === "thread.turn-interrupt-requested"); + yield* harness.waitForDomainEvent( + (event) => event.type === "thread.turn-interrupt-requested", + ); const interruptCalls = yield* waitForSync( () => harness.adapterHarness!.getInterruptCalls(THREAD_ID), @@ -1221,7 +1224,8 @@ it.live("reverts claudeCode turns and rolls back provider conversation state", ( yield* harness.waitForThread( THREAD_ID, - (entry) => entry.latestTurn?.turnId === "turn-1" && entry.session?.threadId === "thread-1", + (entry) => + entry.latestTurn?.turnId === "turn-1" && entry.session?.threadId === "thread-1", ); yield* harness.adapterHarness!.queueTurnResponse(THREAD_ID, { diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts index 6d5a8e29f..2be1b28fa 100644 --- a/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts @@ -9,9 +9,7 @@ import { ApprovalRequestId, ThreadId } from "@t3tools/contracts"; import { assert, describe, it } from "@effect/vitest"; import { Effect, Fiber, Random, Stream } from "effect"; -import { - ProviderAdapterValidationError, -} from "../Errors.ts"; +import { ProviderAdapterValidationError } from "../Errors.ts"; import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts"; import { makeClaudeCodeAdapterLive, @@ -161,7 +159,6 @@ function makeDeterministicRandomService(seed = 0x1234_5678): { }; } - const THREAD_ID = ThreadId.makeUnsafe("thread-claude-1"); const RESUME_THREAD_ID = ThreadId.makeUnsafe("thread-claude-resume"); @@ -372,94 +369,99 @@ describe("ClaudeCodeAdapterLive", () => { ); }); - it.effect("emits completion only after turn result when assistant frames arrive before deltas", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeCodeAdapter; + it.effect( + "emits completion only after turn result when assistant frames arrive before deltas", + () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeCodeAdapter; - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 9).pipe( - Stream.runCollect, - Effect.forkChild, - ); + const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 9).pipe( + Stream.runCollect, + Effect.forkChild, + ); - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: "claudeCode", - runtimeMode: "full-access", - }); - - const turn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); + const session = yield* adapter.startSession({ + threadId: THREAD_ID, + provider: "claudeCode", + runtimeMode: "full-access", + }); - harness.query.emit({ - type: "assistant", - session_id: "sdk-session-early-assistant", - uuid: "assistant-early", - parent_tool_use_id: null, - message: { - id: "assistant-message-early", - content: [{ type: "tool_use", id: "tool-early", name: "Read", input: { path: "a.ts" } }], - }, - } as unknown as SDKMessage); + const turn = yield* adapter.sendTurn({ + threadId: session.threadId, + input: "hello", + attachments: [], + }); - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-early-assistant", - uuid: "stream-early", - parent_tool_use_id: null, - event: { - type: "content_block_delta", - index: 0, - delta: { - type: "text_delta", - text: "Late text", + harness.query.emit({ + type: "assistant", + session_id: "sdk-session-early-assistant", + uuid: "assistant-early", + parent_tool_use_id: null, + message: { + id: "assistant-message-early", + content: [ + { type: "tool_use", id: "tool-early", name: "Read", input: { path: "a.ts" } }, + ], }, - }, - } as unknown as SDKMessage); + } as unknown as SDKMessage); + + harness.query.emit({ + type: "stream_event", + session_id: "sdk-session-early-assistant", + uuid: "stream-early", + parent_tool_use_id: null, + event: { + type: "content_block_delta", + index: 0, + delta: { + type: "text_delta", + text: "Late text", + }, + }, + } as unknown as SDKMessage); + + harness.query.emit({ + type: "result", + subtype: "success", + is_error: false, + errors: [], + session_id: "sdk-session-early-assistant", + uuid: "result-early", + } as unknown as SDKMessage); + + const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); + assert.deepEqual( + runtimeEvents.map((event) => event.type), + [ + "session.started", + "session.configured", + "session.state.changed", + "turn.started", + "thread.started", + "item.updated", + "content.delta", + "item.completed", + "turn.completed", + ], + ); - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-early-assistant", - uuid: "result-early", - } as unknown as SDKMessage); + const deltaIndex = runtimeEvents.findIndex((event) => event.type === "content.delta"); + const completedIndex = runtimeEvents.findIndex((event) => event.type === "item.completed"); + assert.equal(deltaIndex >= 0 && completedIndex >= 0 && deltaIndex < completedIndex, true); - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - assert.deepEqual( - runtimeEvents.map((event) => event.type), - [ - "session.started", - "session.configured", - "session.state.changed", - "turn.started", - "thread.started", - "item.updated", - "content.delta", - "item.completed", - "turn.completed", - ], + const deltaEvent = runtimeEvents[deltaIndex]; + assert.equal(deltaEvent?.type, "content.delta"); + if (deltaEvent?.type === "content.delta") { + assert.equal(deltaEvent.payload.delta, "Late text"); + assert.equal(String(deltaEvent.turnId), String(turn.turnId)); + } + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), ); - - const deltaIndex = runtimeEvents.findIndex((event) => event.type === "content.delta"); - const completedIndex = runtimeEvents.findIndex((event) => event.type === "item.completed"); - assert.equal(deltaIndex >= 0 && completedIndex >= 0 && deltaIndex < completedIndex, true); - - const deltaEvent = runtimeEvents[deltaIndex]; - assert.equal(deltaEvent?.type, "content.delta"); - if (deltaEvent?.type === "content.delta") { - assert.equal(deltaEvent.payload.delta, "Late text"); - assert.equal(String(deltaEvent.turnId), String(turn.turnId)); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); + }, + ); it.effect("falls back to assistant payload text when stream deltas are absent", () => { const harness = makeHarness(); @@ -731,10 +733,7 @@ describe("ClaudeCodeAdapterLive", () => { runtimeMode: "full-access", }); - assert.equal( - "resume" in (session.resumeCursor as Record), - false, - ); + assert.equal("resume" in (session.resumeCursor as Record), false); const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.resume, undefined); @@ -744,84 +743,87 @@ describe("ClaudeCodeAdapterLive", () => { ); }); - it.effect("supports rollbackThread by trimming in-memory turns and preserving earlier turns", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeCodeAdapter; + it.effect( + "supports rollbackThread by trimming in-memory turns and preserving earlier turns", + () => { + const harness = makeHarness(); + return Effect.gen(function* () { + const adapter = yield* ClaudeCodeAdapter; - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: "claudeCode", - runtimeMode: "full-access", - }); - - const firstTurn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "first", - attachments: [], - }); - - const firstCompletedFiber = yield* Stream.filter(adapter.streamEvents, (event) => event.type === "turn.completed").pipe( - Stream.runHead, - Effect.forkChild, - ); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-rollback", - uuid: "result-first", - } as unknown as SDKMessage); - - const firstCompleted = yield* Fiber.join(firstCompletedFiber); - assert.equal(firstCompleted._tag, "Some"); - if (firstCompleted._tag === "Some" && firstCompleted.value.type === "turn.completed") { - assert.equal(String(firstCompleted.value.turnId), String(firstTurn.turnId)); - } + const session = yield* adapter.startSession({ + threadId: THREAD_ID, + provider: "claudeCode", + runtimeMode: "full-access", + }); - const secondTurn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "second", - attachments: [], - }); + const firstTurn = yield* adapter.sendTurn({ + threadId: session.threadId, + input: "first", + attachments: [], + }); - const secondCompletedFiber = yield* Stream.filter(adapter.streamEvents, (event) => event.type === "turn.completed").pipe( - Stream.runHead, - Effect.forkChild, - ); + const firstCompletedFiber = yield* Stream.filter( + adapter.streamEvents, + (event) => event.type === "turn.completed", + ).pipe(Stream.runHead, Effect.forkChild); + + harness.query.emit({ + type: "result", + subtype: "success", + is_error: false, + errors: [], + session_id: "sdk-session-rollback", + uuid: "result-first", + } as unknown as SDKMessage); + + const firstCompleted = yield* Fiber.join(firstCompletedFiber); + assert.equal(firstCompleted._tag, "Some"); + if (firstCompleted._tag === "Some" && firstCompleted.value.type === "turn.completed") { + assert.equal(String(firstCompleted.value.turnId), String(firstTurn.turnId)); + } - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-rollback", - uuid: "result-second", - } as unknown as SDKMessage); + const secondTurn = yield* adapter.sendTurn({ + threadId: session.threadId, + input: "second", + attachments: [], + }); - const secondCompleted = yield* Fiber.join(secondCompletedFiber); - assert.equal(secondCompleted._tag, "Some"); - if (secondCompleted._tag === "Some" && secondCompleted.value.type === "turn.completed") { - assert.equal(String(secondCompleted.value.turnId), String(secondTurn.turnId)); - } + const secondCompletedFiber = yield* Stream.filter( + adapter.streamEvents, + (event) => event.type === "turn.completed", + ).pipe(Stream.runHead, Effect.forkChild); + + harness.query.emit({ + type: "result", + subtype: "success", + is_error: false, + errors: [], + session_id: "sdk-session-rollback", + uuid: "result-second", + } as unknown as SDKMessage); + + const secondCompleted = yield* Fiber.join(secondCompletedFiber); + assert.equal(secondCompleted._tag, "Some"); + if (secondCompleted._tag === "Some" && secondCompleted.value.type === "turn.completed") { + assert.equal(String(secondCompleted.value.turnId), String(secondTurn.turnId)); + } - const threadBeforeRollback = yield* adapter.readThread(session.threadId); - assert.equal(threadBeforeRollback.turns.length, 2); + const threadBeforeRollback = yield* adapter.readThread(session.threadId); + assert.equal(threadBeforeRollback.turns.length, 2); - const rolledBack = yield* adapter.rollbackThread(session.threadId, 1); - assert.equal(rolledBack.turns.length, 1); - assert.equal(rolledBack.turns[0]?.id, firstTurn.turnId); + const rolledBack = yield* adapter.rollbackThread(session.threadId, 1); + assert.equal(rolledBack.turns.length, 1); + assert.equal(rolledBack.turns[0]?.id, firstTurn.turnId); - const threadAfterRollback = yield* adapter.readThread(session.threadId); - assert.equal(threadAfterRollback.turns.length, 1); - assert.equal(threadAfterRollback.turns[0]?.id, firstTurn.turnId); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); + const threadAfterRollback = yield* adapter.readThread(session.threadId); + assert.equal(threadAfterRollback.turns.length, 1); + assert.equal(threadAfterRollback.turns[0]?.id, firstTurn.turnId); + }).pipe( + Effect.provideService(Random.Random, makeDeterministicRandomService()), + Effect.provide(harness.layer), + ); + }, + ); it.effect("updates model on sendTurn when model override is provided", () => { const harness = makeHarness(); @@ -913,7 +915,10 @@ describe("ClaudeCodeAdapterLive", () => { assert.equal(turnCompleted._tag, "Some"); assert.equal(nativeEvents.length > 0, true); - assert.equal(nativeEvents.some((record) => record.event?.provider === "claudeCode"), true); + assert.equal( + nativeEvents.some((record) => record.event?.provider === "claudeCode"), + true, + ); assert.equal( nativeEvents.some( (record) => diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts index e6bccec4e..c4bc93a9f 100644 --- a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts @@ -372,11 +372,7 @@ function toSessionError( return undefined; } -function toRequestError( - threadId: ThreadId, - method: string, - cause: unknown, -): ProviderAdapterError { +function toRequestError(threadId: ThreadId, method: string, cause: unknown): ProviderAdapterError { const sessionError = toSessionError(threadId, cause); if (sessionError) { return sessionError; @@ -489,40 +485,42 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { const observedAt = new Date().toISOString(); const itemId = sdkNativeItemId(message); - yield* nativeEventLogger - .write( - { - observedAt, - event: { - id: - "uuid" in message && typeof message.uuid === "string" - ? message.uuid - : crypto.randomUUID(), - kind: "notification", - provider: PROVIDER, - createdAt: observedAt, - method: sdkNativeMethod(message), - ...(typeof message.session_id === "string" - ? { providerThreadId: message.session_id } - : {}), - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - ...(itemId ? { itemId: ProviderItemId.makeUnsafe(itemId) } : {}), - payload: message, - }, + yield* nativeEventLogger.write( + { + observedAt, + event: { + id: + "uuid" in message && typeof message.uuid === "string" + ? message.uuid + : crypto.randomUUID(), + kind: "notification", + provider: PROVIDER, + createdAt: observedAt, + method: sdkNativeMethod(message), + ...(typeof message.session_id === "string" + ? { providerThreadId: message.session_id } + : {}), + ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), + ...(itemId ? { itemId: ProviderItemId.makeUnsafe(itemId) } : {}), + payload: message, }, - null, - ); + }, + null, + ); }); const snapshotThread = ( context: ClaudeSessionContext, - ): Effect.Effect<{ - threadId: ThreadId; - turns: ReadonlyArray<{ - id: TurnId; - items: ReadonlyArray; - }>; - }, ProviderAdapterValidationError> => + ): Effect.Effect< + { + threadId: ThreadId; + turns: ReadonlyArray<{ + id: TurnId; + items: ReadonlyArray; + }>; + }, + ProviderAdapterValidationError + > => Effect.gen(function* () { const threadId = context.session.threadId; if (!threadId) { @@ -853,7 +851,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { type: "item.started", eventId: stamp.eventId, provider: PROVIDER, - createdAt: stamp.createdAt, + createdAt: stamp.createdAt, threadId: context.session.threadId, ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), itemId: asRuntimeItemId(tool.itemId), @@ -894,7 +892,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { type: "item.completed", eventId: stamp.eventId, provider: PROVIDER, - createdAt: stamp.createdAt, + createdAt: stamp.createdAt, threadId: context.session.threadId, ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), itemId: asRuntimeItemId(tool.itemId), @@ -1302,7 +1300,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { type: "request.resolved", eventId: stamp.eventId, provider: PROVIDER, - createdAt: stamp.createdAt, + createdAt: stamp.createdAt, threadId: context.session.threadId, ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), requestId: asRuntimeRequestId(requestId), @@ -1341,7 +1339,7 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { type: "session.exited", eventId: stamp.eventId, provider: PROVIDER, - createdAt: stamp.createdAt, + createdAt: stamp.createdAt, threadId: context.session.threadId, payload: { reason: "Session stopped", @@ -1440,9 +1438,11 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { type: "request.opened", eventId: requestedStamp.eventId, provider: PROVIDER, - createdAt: requestedStamp.createdAt, + createdAt: requestedStamp.createdAt, threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), + ...(context.turnState + ? { turnId: asCanonicalTurnId(context.turnState.turnId) } + : {}), requestId: asRuntimeRequestId(requestId), payload: { requestType, @@ -1454,10 +1454,12 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { }, }, providerRefs: { - ...(context.session.threadId + ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), - ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), + ...(context.turnState + ? { providerTurnId: String(context.turnState.turnId) } + : {}), providerRequestId: requestId, }, raw: { @@ -1492,19 +1494,23 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { type: "request.resolved", eventId: resolvedStamp.eventId, provider: PROVIDER, - createdAt: resolvedStamp.createdAt, + createdAt: resolvedStamp.createdAt, threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), + ...(context.turnState + ? { turnId: asCanonicalTurnId(context.turnState.turnId) } + : {}), requestId: asRuntimeRequestId(requestId), payload: { requestType, decision, }, providerRefs: { - ...(context.session.threadId + ...(context.session.threadId ? { providerThreadId: context.session.threadId } : {}), - ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), + ...(context.turnState + ? { providerTurnId: String(context.turnState.turnId) } + : {}), providerRequestId: requestId, }, raw: { diff --git a/apps/server/src/provider/Layers/ProviderService.test.ts b/apps/server/src/provider/Layers/ProviderService.test.ts index a2b2348f2..373083386 100644 --- a/apps/server/src/provider/Layers/ProviderService.test.ts +++ b/apps/server/src/provider/Layers/ProviderService.test.ts @@ -224,7 +224,7 @@ function makeProviderServiceLayer() { ? Effect.succeed(codex.adapter) : provider === "claudeCode" ? Effect.succeed(claude.adapter) - : Effect.fail(new ProviderUnsupportedError({ provider })), + : Effect.fail(new ProviderUnsupportedError({ provider })), listProviders: () => Effect.succeed(["codex", "claudeCode"]), }; diff --git a/apps/server/src/provider/Services/ClaudeCodeAdapter.ts b/apps/server/src/provider/Services/ClaudeCodeAdapter.ts index 80fb8771d..6ef687606 100644 --- a/apps/server/src/provider/Services/ClaudeCodeAdapter.ts +++ b/apps/server/src/provider/Services/ClaudeCodeAdapter.ts @@ -29,4 +29,3 @@ export class ClaudeCodeAdapter extends ServiceMap.Service< ClaudeCodeAdapter, ClaudeCodeAdapterShape >()("t3/provider/Services/ClaudeCodeAdapter") {} - diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 7b0111e25..f56d6a287 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -519,12 +519,7 @@ export default function ChatView({ threadId }: ChatViewProps) { customModelsForSelectedProvider, draftModel, ) as ModelSlug; - }, [ - baseThreadModel, - composerDraft.model, - customModelsForSelectedProvider, - selectedProvider, - ]); + }, [baseThreadModel, composerDraft.model, customModelsForSelectedProvider, selectedProvider]); const reasoningOptions = getReasoningEffortOptions(selectedProvider); const supportsReasoningEffort = reasoningOptions.length > 0; const selectedEffort = composerDraft.effort ?? getDefaultReasoningEffort(selectedProvider); diff --git a/packages/shared/src/model.test.ts b/packages/shared/src/model.test.ts index 224b86e56..785374008 100644 --- a/packages/shared/src/model.test.ts +++ b/packages/shared/src/model.test.ts @@ -49,17 +49,11 @@ describe("normalizeModelSlug", () => { it("uses provider-specific aliases", () => { expect(normalizeModelSlug("sonnet", "claudeCode")).toBe("claude-sonnet-4-6"); expect(normalizeModelSlug("opus-4.6", "claudeCode")).toBe("claude-opus-4-6"); - expect(normalizeModelSlug("claude-haiku-4-5-20251001", "claudeCode")).toBe( - "claude-haiku-4-5", - ); + expect(normalizeModelSlug("claude-haiku-4-5-20251001", "claudeCode")).toBe("claude-haiku-4-5"); expect(normalizeModelSlug("composer", "cursor")).toBe("composer-1.5"); - expect(normalizeModelSlug("gpt-5.3-codex-spark", "cursor")).toBe( - "gpt-5.3-codex-spark-preview", - ); + expect(normalizeModelSlug("gpt-5.3-codex-spark", "cursor")).toBe("gpt-5.3-codex-spark-preview"); expect(normalizeModelSlug("gemini-3.1", "cursor")).toBe("gemini-3.1"); - expect(normalizeModelSlug("claude-4.6-sonnet-thinking", "cursor")).toBe( - "sonnet-4.6-thinking", - ); + expect(normalizeModelSlug("claude-4.6-sonnet-thinking", "cursor")).toBe("sonnet-4.6-thinking"); }); }); @@ -88,9 +82,7 @@ describe("resolveModelSlug", () => { expect(resolveModelSlugForProvider("claudeCode", "gpt-5.3-codex")).toBe( DEFAULT_MODEL_BY_PROVIDER.claudeCode, ); - expect(resolveModelSlugForProvider("cursor", undefined)).toBe( - DEFAULT_MODEL_BY_PROVIDER.cursor, - ); + expect(resolveModelSlugForProvider("cursor", undefined)).toBe(DEFAULT_MODEL_BY_PROVIDER.cursor); expect(resolveModelSlugForProvider("cursor", "composer")).toBe("composer-1.5"); expect(resolveModelSlugForProvider("cursor", "gpt-5.3-codex-high-fast")).toBe( "gpt-5.3-codex-high-fast", @@ -156,9 +148,7 @@ describe("cursor model selection", () => { describe("getReasoningEffortOptions", () => { it("returns codex reasoning options for codex", () => { - expect(getReasoningEffortOptions("codex")).toEqual( - REASONING_EFFORT_OPTIONS_BY_PROVIDER.codex, - ); + expect(getReasoningEffortOptions("codex")).toEqual(REASONING_EFFORT_OPTIONS_BY_PROVIDER.codex); }); it("returns no reasoning options for claudeCode", () => { @@ -172,14 +162,10 @@ describe("getReasoningEffortOptions", () => { describe("getDefaultReasoningEffort", () => { it("returns provider-scoped defaults", () => { - expect(getDefaultReasoningEffort("codex")).toBe( - DEFAULT_REASONING_EFFORT_BY_PROVIDER.codex, - ); + expect(getDefaultReasoningEffort("codex")).toBe(DEFAULT_REASONING_EFFORT_BY_PROVIDER.codex); expect(getDefaultReasoningEffort("claudeCode")).toBe( DEFAULT_REASONING_EFFORT_BY_PROVIDER.claudeCode, ); - expect(getDefaultReasoningEffort("cursor")).toBe( - DEFAULT_REASONING_EFFORT_BY_PROVIDER.cursor, - ); + expect(getDefaultReasoningEffort("cursor")).toBe(DEFAULT_REASONING_EFFORT_BY_PROVIDER.cursor); }); }); From 040011dbc72ae70071bef16ec2f57428f84814b1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 16 Mar 2026 10:35:53 -0700 Subject: [PATCH 11/23] Remove Cursor provider support across server and web - drop `cursor` from provider/model contracts and runtime validation - remove Cursor-specific UI options, settings, and model normalization paths - update server/web tests and fixtures to cover Codex + Claude Code only --- .../TestProviderAdapter.integration.ts | 2 +- .../Layers/CheckpointReactor.test.ts | 2 +- .../Layers/ProviderCommandReactor.test.ts | 44 +--- .../Layers/ProviderCommandReactor.ts | 3 +- .../Layers/ProviderRuntimeIngestion.test.ts | 2 +- .../Layers/ProviderAdapterRegistry.ts | 2 +- .../provider/Layers/ProviderService.test.ts | 4 +- .../Layers/ProviderSessionDirectory.test.ts | 4 +- .../Layers/ProviderSessionDirectory.ts | 2 +- apps/web/src/appSettings.test.ts | 8 +- apps/web/src/appSettings.ts | 5 - apps/web/src/components/ChatView.logic.ts | 2 - apps/web/src/components/ChatView.tsx | 3 +- .../components/chat/ProviderModelPicker.tsx | 3 +- apps/web/src/composerDraftStore.ts | 2 +- apps/web/src/routes/_chat.settings.tsx | 7 - apps/web/src/session-logic.test.ts | 9 +- apps/web/src/session-logic.ts | 3 +- apps/web/src/store.test.ts | 22 -- apps/web/src/store.ts | 25 +-- packages/contracts/src/model.ts | 69 ------ packages/contracts/src/orchestration.ts | 7 +- packages/contracts/src/provider.test.ts | 25 --- packages/contracts/src/provider.ts | 5 - packages/shared/src/model.test.ts | 73 +------ packages/shared/src/model.ts | 203 ------------------ 26 files changed, 25 insertions(+), 511 deletions(-) diff --git a/apps/server/integration/TestProviderAdapter.integration.ts b/apps/server/integration/TestProviderAdapter.integration.ts index d970bf784..98ad7b8b5 100644 --- a/apps/server/integration/TestProviderAdapter.integration.ts +++ b/apps/server/integration/TestProviderAdapter.integration.ts @@ -35,7 +35,7 @@ export interface TestTurnResponse { export type FixtureProviderRuntimeEvent = { readonly type: string; readonly eventId: EventId; - readonly provider: "codex" | "claudeCode" | "cursor"; + readonly provider: "codex" | "claudeCode"; readonly createdAt: string; readonly threadId: string; readonly turnId?: string | undefined; diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts index 2944012f0..cfde828d3 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts @@ -44,7 +44,7 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); type LegacyProviderRuntimeEvent = { readonly type: string; readonly eventId: EventId; - readonly provider: "codex" | "claudeCode" | "cursor"; + readonly provider: "codex" | "claudeCode"; readonly createdAt: string; readonly threadId: ThreadId; readonly turnId?: string | undefined; diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts index af37cc96a..308205193 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts @@ -96,9 +96,7 @@ describe("ProviderCommandReactor", () => { typeof input === "object" && input !== null && "provider" in input && - (input.provider === "codex" || - input.provider === "claudeCode" || - input.provider === "cursor") + (input.provider === "codex" || input.provider === "claudeCode") ? input.provider : "codex"; const resumeCursor = @@ -131,7 +129,7 @@ describe("ProviderCommandReactor", () => { : "full-access", ...(model !== undefined ? { model } : {}), threadId, - resumeCursor: resumeCursor ?? { opaque: `cursor-${sessionIndex}` }, + resumeCursor: resumeCursor ?? { opaque: `resume-${sessionIndex}` }, createdAt: now, updatedAt: now, }; @@ -428,42 +426,6 @@ describe("ProviderCommandReactor", () => { expect(thread?.session?.threadId).toBe("thread-1"); }); - it("starts first turn with cursor provider when provider is specified", async () => { - const harness = await createHarness(); - const now = new Date().toISOString(); - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.makeUnsafe("cmd-turn-start-provider-cursor"), - threadId: ThreadId.makeUnsafe("thread-1"), - message: { - messageId: asMessageId("user-message-provider-cursor"), - role: "user", - text: "hello cursor", - attachments: [], - }, - provider: "cursor", - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }), - ); - - await waitFor(() => harness.startSession.mock.calls.length === 1); - await waitFor(() => harness.sendTurn.mock.calls.length === 1); - expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({ - provider: "cursor", - cwd: "/tmp/provider-project", - model: "gpt-5-codex", - runtimeMode: "approval-required", - }); - - const readModel = await Effect.runPromise(harness.engine.getReadModel()); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); - expect(thread?.session?.providerName).toBe("cursor"); - expect(thread?.session?.threadId).toBe("thread-1"); - }); it("reuses the same provider session when runtime mode is unchanged", async () => { const harness = await createHarness(); const now = new Date().toISOString(); @@ -584,7 +546,7 @@ describe("ProviderCommandReactor", () => { expect(harness.stopSession.mock.calls.length).toBe(0); expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({ threadId: ThreadId.makeUnsafe("thread-1"), - resumeCursor: { opaque: "cursor-1" }, + resumeCursor: { opaque: "resume-1" }, runtimeMode: "approval-required", }); expect(harness.sendTurn.mock.calls[1]?.[0]).toMatchObject({ diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index eb976f5d1..5a52d41be 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -207,8 +207,7 @@ const make = Effect.gen(function* () { const desiredRuntimeMode = thread.runtimeMode; const currentProvider: ProviderKind | undefined = thread.session?.providerName === "codex" || - thread.session?.providerName === "claudeCode" || - thread.session?.providerName === "cursor" + thread.session?.providerName === "claudeCode" ? thread.session.providerName : undefined; const preferredProvider: ProviderKind | undefined = options?.provider ?? currentProvider; diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts index 63be516d0..874458ff5 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts @@ -45,7 +45,7 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); type LegacyProviderRuntimeEvent = { readonly type: string; readonly eventId: EventId; - readonly provider: "codex" | "claudeCode" | "cursor"; + readonly provider: "codex" | "claudeCode"; readonly createdAt: string; readonly threadId: ThreadId; readonly turnId?: string | undefined; diff --git a/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts b/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts index 61fa2d18c..37c430951 100644 --- a/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts +++ b/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts @@ -1,7 +1,7 @@ /** * ProviderAdapterRegistryLive - In-memory provider adapter lookup layer. * - * Binds provider kinds (codex/cursor/...) to concrete adapter services. + * Binds provider kinds (codex/claudeCode/...) to concrete adapter services. * This layer only performs adapter lookup; it does not route session-scoped * calls or own provider lifecycle workflows. * diff --git a/apps/server/src/provider/Layers/ProviderService.test.ts b/apps/server/src/provider/Layers/ProviderService.test.ts index 373083386..174aefc5f 100644 --- a/apps/server/src/provider/Layers/ProviderService.test.ts +++ b/apps/server/src/provider/Layers/ProviderService.test.ts @@ -52,7 +52,7 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); type LegacyProviderRuntimeEvent = { readonly type: string; readonly eventId: EventId; - readonly provider: "codex" | "claudeCode" | "cursor"; + readonly provider: "codex" | "claudeCode"; readonly createdAt: string; readonly threadId: ThreadId; readonly turnId?: string | undefined; @@ -74,7 +74,7 @@ function makeFakeCodexAdapter(provider: ProviderKind = "codex") { status: "ready", runtimeMode: input.runtimeMode, threadId: input.threadId, - resumeCursor: input.resumeCursor ?? { opaque: `cursor-${String(input.threadId)}` }, + resumeCursor: input.resumeCursor ?? { opaque: `resume-${String(input.threadId)}` }, cwd: input.cwd ?? process.cwd(), createdAt: now, updatedAt: now, diff --git a/apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts b/apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts index 1882c1cc0..efabae08b 100644 --- a/apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts +++ b/apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts @@ -141,8 +141,8 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL yield* runtimeRepository.upsert({ threadId, - providerName: "cursor", - adapterKey: "cursor", + providerName: "claudeCode", + adapterKey: "claudeCode", runtimeMode: "full-access", status: "running", lastSeenAt: new Date().toISOString(), diff --git a/apps/server/src/provider/Layers/ProviderSessionDirectory.ts b/apps/server/src/provider/Layers/ProviderSessionDirectory.ts index 3f675aae8..71b6d3c42 100644 --- a/apps/server/src/provider/Layers/ProviderSessionDirectory.ts +++ b/apps/server/src/provider/Layers/ProviderSessionDirectory.ts @@ -22,7 +22,7 @@ function decodeProviderKind( providerName: string, operation: string, ): Effect.Effect { - if (providerName === "codex" || providerName === "claudeCode" || providerName === "cursor") { + if (providerName === "codex" || providerName === "claudeCode") { return Effect.succeed(providerName); } return Effect.fail( diff --git a/apps/web/src/appSettings.test.ts b/apps/web/src/appSettings.test.ts index a82e972bc..da55af6ff 100644 --- a/apps/web/src/appSettings.test.ts +++ b/apps/web/src/appSettings.test.ts @@ -21,15 +21,11 @@ describe("normalizeCustomModelSlugs", () => { ).toEqual(["custom/internal-model"]); }); - it("normalizes provider-specific aliases for claude and cursor", () => { + it("normalizes provider-specific aliases for claude", () => { expect(normalizeCustomModelSlugs(["sonnet"], "claudeCode")).toEqual([]); expect(normalizeCustomModelSlugs(["claude/custom-sonnet"], "claudeCode")).toEqual([ "claude/custom-sonnet", ]); - expect(normalizeCustomModelSlugs(["composer"], "cursor")).toEqual([]); - expect(normalizeCustomModelSlugs(["cursor/custom-model"], "cursor")).toEqual([ - "cursor/custom-model", - ]); }); }); @@ -86,9 +82,7 @@ describe("timestamp format defaults", () => { describe("provider-specific custom models", () => { it("includes provider-specific custom slugs in non-codex model lists", () => { const claudeOptions = getAppModelOptions("claudeCode", ["claude/custom-opus"]); - const cursorOptions = getAppModelOptions("cursor", ["cursor/custom-model"]); expect(claudeOptions.some((option) => option.slug === "claude/custom-opus")).toBe(true); - expect(cursorOptions.some((option) => option.slug === "cursor/custom-model")).toBe(true); }); }); diff --git a/apps/web/src/appSettings.ts b/apps/web/src/appSettings.ts index d28511b5b..5c3e80ff7 100644 --- a/apps/web/src/appSettings.ts +++ b/apps/web/src/appSettings.ts @@ -13,7 +13,6 @@ export const DEFAULT_TIMESTAMP_FORMAT: TimestampFormat = "locale"; const BUILT_IN_MODEL_SLUGS_BY_PROVIDER: Record> = { codex: new Set(getModelOptions("codex").map((option) => option.slug)), claudeCode: new Set(getModelOptions("claudeCode").map((option) => option.slug)), - cursor: new Set(getModelOptions("cursor").map((option) => option.slug)), }; const AppSettingsSchema = Schema.Struct({ @@ -39,9 +38,6 @@ const AppSettingsSchema = Schema.Struct({ customClaudeModels: Schema.Array(Schema.String).pipe( Schema.withConstructorDefault(() => Option.some([])), ), - customCursorModels: Schema.Array(Schema.String).pipe( - Schema.withConstructorDefault(() => Option.some([])), - ), }); export type AppSettings = typeof AppSettingsSchema.Type; export interface AppModelOption { @@ -86,7 +82,6 @@ function normalizeAppSettings(settings: AppSettings): AppSettings { ...settings, customCodexModels: normalizeCustomModelSlugs(settings.customCodexModels, "codex"), customClaudeModels: normalizeCustomModelSlugs(settings.customClaudeModels, "claudeCode"), - customCursorModels: normalizeCustomModelSlugs(settings.customCursorModels, "cursor"), }; } export function getAppModelOptions( diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 114da4b69..323b4dc0a 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -119,11 +119,9 @@ export function cloneComposerImageForRetry( export function getCustomModelOptionsByProvider(settings: { customCodexModels: readonly string[]; customClaudeModels: readonly string[]; - customCursorModels: readonly string[]; }): Record> { return { codex: getAppModelOptions("codex", settings.customCodexModels), claudeCode: getAppModelOptions("claudeCode", settings.customClaudeModels), - cursor: getAppModelOptions("cursor", settings.customCursorModels), }; } diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index f56d6a287..a4be9c185 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -504,9 +504,8 @@ export default function ChatView({ threadId }: ChatViewProps) { () => ({ codex: settings.customCodexModels, claudeCode: settings.customClaudeModels, - cursor: settings.customCursorModels, }), - [settings.customClaudeModels, settings.customCodexModels, settings.customCursorModels], + [settings.customClaudeModels, settings.customCodexModels], ); const customModelsForSelectedProvider = customModelsByProvider[selectedProvider]; const selectedModel = useMemo(() => { diff --git a/apps/web/src/components/chat/ProviderModelPicker.tsx b/apps/web/src/components/chat/ProviderModelPicker.tsx index 9bc034991..a15f84e18 100644 --- a/apps/web/src/components/chat/ProviderModelPicker.tsx +++ b/apps/web/src/components/chat/ProviderModelPicker.tsx @@ -17,7 +17,7 @@ import { MenuSubTrigger, MenuTrigger, } from "../ui/menu"; -import { ClaudeAI, CursorIcon, Gemini, Icon, OpenAI, OpenCodeIcon } from "../Icons"; +import { ClaudeAI, Gemini, Icon, OpenAI, OpenCodeIcon } from "../Icons"; import { cn } from "~/lib/utils"; function isAvailableProviderOption(option: (typeof PROVIDER_OPTIONS)[number]): option is { @@ -64,7 +64,6 @@ function resolveModelForProviderPicker( const PROVIDER_ICON_BY_PROVIDER: Record = { codex: OpenAI, claudeCode: ClaudeAI, - cursor: CursorIcon, }; export const AVAILABLE_PROVIDER_OPTIONS = PROVIDER_OPTIONS.filter(isAvailableProviderOption); diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index e012990ba..9e85d49dc 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -246,7 +246,7 @@ function shouldRemoveDraft(draft: ComposerThreadDraftState): boolean { } function normalizeProviderKind(value: unknown): ProviderKind | null { - return value === "codex" || value === "claudeCode" || value === "cursor" ? value : null; + return value === "codex" || value === "claudeCode" ? value : null; } function revokeObjectPreviewUrl(previewUrl: string): void { diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index bdce981e6..62e9ed8ce 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -76,8 +76,6 @@ function getCustomModelsForProvider( switch (provider) { case "claudeCode": return settings.customClaudeModels; - case "cursor": - return settings.customCursorModels; case "codex": default: return settings.customCodexModels; @@ -91,8 +89,6 @@ function getDefaultCustomModelsForProvider( switch (provider) { case "claudeCode": return defaults.customClaudeModels; - case "cursor": - return defaults.customCursorModels; case "codex": default: return defaults.customCodexModels; @@ -103,8 +99,6 @@ function patchCustomModels(provider: ProviderKind, models: string[]) { switch (provider) { case "claudeCode": return { customClaudeModels: models }; - case "cursor": - return { customCursorModels: models }; case "codex": default: return { customCodexModels: models }; @@ -122,7 +116,6 @@ function SettingsRouteView() { >({ codex: "", claudeCode: "", - cursor: "", }); const [customModelErrorByProvider, setCustomModelErrorByProvider] = useState< Partial> diff --git a/apps/web/src/session-logic.test.ts b/apps/web/src/session-logic.test.ts index 21676828e..a8315c52b 100644 --- a/apps/web/src/session-logic.test.ts +++ b/apps/web/src/session-logic.test.ts @@ -673,23 +673,16 @@ describe("deriveActiveWorkStartedAt", () => { }); describe("PROVIDER_OPTIONS", () => { - it("advertises Claude Code on the Claude stack while keeping Cursor as a placeholder", () => { + it("advertises the supported providers", () => { const claude = PROVIDER_OPTIONS.find((option) => option.value === "claudeCode"); - const cursor = PROVIDER_OPTIONS.find((option) => option.value === "cursor"); expect(PROVIDER_OPTIONS).toEqual([ { value: "codex", label: "Codex", available: true }, { value: "claudeCode", label: "Claude Code", available: true }, - { value: "cursor", label: "Cursor", available: false }, ]); expect(claude).toEqual({ value: "claudeCode", label: "Claude Code", available: true, }); - expect(cursor).toEqual({ - value: "cursor", - label: "Cursor", - available: false, - }); }); }); diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts index 4e75c09dd..67af9af71 100644 --- a/apps/web/src/session-logic.ts +++ b/apps/web/src/session-logic.ts @@ -18,7 +18,7 @@ import type { TurnDiffSummary, } from "./types"; -export type ProviderPickerKind = ProviderKind | "claudeCode" | "cursor"; +export type ProviderPickerKind = ProviderKind; export const PROVIDER_OPTIONS: Array<{ value: ProviderPickerKind; @@ -27,7 +27,6 @@ export const PROVIDER_OPTIONS: Array<{ }> = [ { value: "codex", label: "Codex", available: true }, { value: "claudeCode", label: "Claude Code", available: true }, - { value: "cursor", label: "Cursor", available: false }, ]; export interface WorkLogEntry { diff --git a/apps/web/src/store.test.ts b/apps/web/src/store.test.ts index 5fcb258a6..204db4564 100644 --- a/apps/web/src/store.test.ts +++ b/apps/web/src/store.test.ts @@ -226,28 +226,6 @@ describe("store read model sync", () => { expect(next.threads[0]?.model).toBe("claude-sonnet-4-6"); }); - it("resolves cursor aliases when session provider is cursor", () => { - const initialState = makeState(makeThread()); - const readModel = makeReadModel( - makeReadModelThread({ - model: "composer", - session: { - threadId: ThreadId.makeUnsafe("thread-1"), - status: "ready", - providerName: "cursor", - runtimeMode: "approval-required", - activeTurnId: null, - lastError: null, - updatedAt: "2026-02-27T00:00:00.000Z", - }, - }), - ); - - const next = syncServerReadModel(initialState, readModel); - - expect(next.threads[0]?.model).toBe("composer-1.5"); - }); - it("preserves the current project order when syncing incoming read model updates", () => { const project1 = ProjectId.makeUnsafe("project-1"); const project2 = ProjectId.makeUnsafe("project-2"); diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts index dadd24b28..4c6f43a2b 100644 --- a/apps/web/src/store.ts +++ b/apps/web/src/store.ts @@ -189,7 +189,7 @@ function toLegacySessionStatus( } function toLegacyProvider(providerName: string | null): ProviderKind { - if (providerName === "codex" || providerName === "claudeCode" || providerName === "cursor") { + if (providerName === "codex" || providerName === "claudeCode") { return providerName; } return "codex"; @@ -199,28 +199,14 @@ const CODEX_MODEL_SLUGS = new Set(getModelOptions("codex").map((option) const CLAUDE_MODEL_SLUGS = new Set( getModelOptions("claudeCode").map((option) => option.slug), ); -const CURSOR_MODEL_SLUGS = new Set(getModelOptions("cursor").map((option) => option.slug)); -const CURSOR_DISTINCT_MODEL_SLUGS = new Set( - [...CURSOR_MODEL_SLUGS].filter( - (slug) => !CODEX_MODEL_SLUGS.has(slug) && !CLAUDE_MODEL_SLUGS.has(slug), - ), -); function inferProviderForThreadModel(input: { readonly model: string; readonly sessionProviderName: string | null; }): ProviderKind { - if ( - input.sessionProviderName === "codex" || - input.sessionProviderName === "claudeCode" || - input.sessionProviderName === "cursor" - ) { + if (input.sessionProviderName === "codex" || input.sessionProviderName === "claudeCode") { return input.sessionProviderName; } - const normalizedCursor = normalizeModelSlug(input.model, "cursor"); - if (normalizedCursor && CURSOR_DISTINCT_MODEL_SLUGS.has(normalizedCursor)) { - return "cursor"; - } const normalizedClaude = normalizeModelSlug(input.model, "claudeCode"); if (normalizedClaude && CLAUDE_MODEL_SLUGS.has(normalizedClaude)) { return "claudeCode"; @@ -229,13 +215,6 @@ function inferProviderForThreadModel(input: { if (normalizedCodex && CODEX_MODEL_SLUGS.has(normalizedCodex)) { return "codex"; } - if ( - input.model.trim().startsWith("composer-") || - input.model.trim().startsWith("gemini-") || - input.model.trim().endsWith("-thinking") - ) { - return "cursor"; - } return input.model.trim().startsWith("claude-") ? "claudeCode" : "codex"; } diff --git a/packages/contracts/src/model.ts b/packages/contracts/src/model.ts index 12423767e..f1acfbb01 100644 --- a/packages/contracts/src/model.ts +++ b/packages/contracts/src/model.ts @@ -1,9 +1,6 @@ import { Schema } from "effect"; import type { ProviderKind } from "./orchestration"; -export const CURSOR_REASONING_OPTIONS = ["low", "normal", "high", "xhigh"] as const; -export type CursorReasoningOption = (typeof CURSOR_REASONING_OPTIONS)[number]; - export const CODEX_REASONING_EFFORT_OPTIONS = ["xhigh", "high", "medium", "low"] as const; export type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORT_OPTIONS)[number]; @@ -18,17 +15,9 @@ export const ClaudeCodeModelOptions = Schema.Struct({ }); export type ClaudeCodeModelOptions = typeof ClaudeCodeModelOptions.Type; -export const CursorModelOptions = Schema.Struct({ - reasoning: Schema.optional(Schema.Literals(CURSOR_REASONING_OPTIONS)), - fastMode: Schema.optional(Schema.Boolean), - thinking: Schema.optional(Schema.Boolean), -}); -export type CursorModelOptions = typeof CursorModelOptions.Type; - export const ProviderModelOptions = Schema.Struct({ codex: Schema.optional(CodexModelOptions), claudeCode: Schema.optional(ClaudeCodeModelOptions), - cursor: Schema.optional(CursorModelOptions), }); export type ProviderModelOptions = typeof ProviderModelOptions.Type; @@ -37,25 +26,6 @@ type ModelOption = { readonly name: string; }; -type CursorModelFamilyOption = { - readonly slug: string; - readonly name: string; -}; - -export const CURSOR_MODEL_FAMILY_OPTIONS = [ - { slug: "auto", name: "Auto" }, - { slug: "composer-1.5", name: "Composer 1.5" }, - { slug: "composer-1", name: "Composer 1" }, - { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, - { slug: "gpt-5.3-codex-spark-preview", name: "GPT-5.3 Codex Spark" }, - { slug: "opus-4.6", name: "Claude 4.6 Opus" }, - { slug: "opus-4.5", name: "Claude 4.5 Opus" }, - { slug: "sonnet-4.6", name: "Claude 4.6 Sonnet" }, - { slug: "gemini-3.1-pro", name: "Gemini 3.1 Pro" }, -] as const satisfies readonly CursorModelFamilyOption[]; - -export type CursorModelFamily = (typeof CURSOR_MODEL_FAMILY_OPTIONS)[number]["slug"]; - export const MODEL_OPTIONS_BY_PROVIDER = { codex: [ { slug: "gpt-5.4", name: "GPT-5.4" }, @@ -69,38 +39,15 @@ export const MODEL_OPTIONS_BY_PROVIDER = { { slug: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" }, { slug: "claude-haiku-4-5", name: "Claude Haiku 4.5" }, ], - cursor: [ - { slug: "auto", name: "Auto" }, - { slug: "composer-1.5", name: "Composer 1.5" }, - { slug: "composer-1", name: "Composer 1" }, - { slug: "gpt-5.3-codex-low", name: "GPT-5.3 Codex Low" }, - { slug: "gpt-5.3-codex-low-fast", name: "GPT-5.3 Codex Low Fast" }, - { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, - { slug: "gpt-5.3-codex-fast", name: "GPT-5.3 Codex Fast" }, - { slug: "gpt-5.3-codex-high", name: "GPT-5.3 Codex High" }, - { slug: "gpt-5.3-codex-high-fast", name: "GPT-5.3 Codex High Fast" }, - { slug: "gpt-5.3-codex-xhigh", name: "GPT-5.3 Codex Extra High" }, - { slug: "gpt-5.3-codex-xhigh-fast", name: "GPT-5.3 Codex Extra High Fast" }, - { slug: "gpt-5.3-codex-spark-preview", name: "GPT-5.3 Codex Spark" }, - { slug: "opus-4.6", name: "Claude 4.6 Opus" }, - { slug: "opus-4.6-thinking", name: "Claude 4.6 Opus (Thinking)" }, - { slug: "opus-4.5", name: "Claude 4.5 Opus" }, - { slug: "opus-4.5-thinking", name: "Claude 4.5 Opus (Thinking)" }, - { slug: "sonnet-4.6", name: "Claude 4.6 Sonnet" }, - { slug: "sonnet-4.6-thinking", name: "Claude 4.6 Sonnet (Thinking)" }, - { slug: "gemini-3.1-pro", name: "Gemini 3.1 Pro" }, - ], } as const satisfies Record; export type ModelOptionsByProvider = typeof MODEL_OPTIONS_BY_PROVIDER; type BuiltInModelSlug = (typeof MODEL_OPTIONS_BY_PROVIDER)[ProviderKind][number]["slug"]; export type ModelSlug = BuiltInModelSlug | (string & {}); -export type CursorModelSlug = (typeof MODEL_OPTIONS_BY_PROVIDER)["cursor"][number]["slug"]; export const DEFAULT_MODEL_BY_PROVIDER: Record = { codex: "gpt-5.4", claudeCode: "claude-sonnet-4-6", - cursor: "opus-4.6-thinking", }; // Backward compatibility for existing Codex-only call sites. @@ -129,30 +76,14 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record; export const DEFAULT_REASONING_EFFORT_BY_PROVIDER = { codex: "high", claudeCode: null, - cursor: null, } as const satisfies Record; diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 5cb308a81..95605306e 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -27,7 +27,7 @@ export const ORCHESTRATION_WS_CHANNELS = { domainEvent: "orchestration.domainEvent", } as const; -export const ProviderKind = Schema.Literals(["codex", "claudeCode", "cursor"]); +export const ProviderKind = Schema.Literals(["codex", "claudeCode"]); export type ProviderKind = typeof ProviderKind.Type; export const ProviderApprovalPolicy = Schema.Literals([ "untrusted", @@ -56,11 +56,6 @@ const ProviderStartOptions = Schema.Struct({ maxThinkingTokens: Schema.optional(NonNegativeInt), }), ), - cursor: Schema.optional( - Schema.Struct({ - binaryPath: Schema.optional(TrimmedNonEmptyString), - }), - ), }); export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]); export type RuntimeMode = typeof RuntimeMode.Type; diff --git a/packages/contracts/src/provider.test.ts b/packages/contracts/src/provider.test.ts index 97e168a33..ab5995649 100644 --- a/packages/contracts/src/provider.test.ts +++ b/packages/contracts/src/provider.test.ts @@ -64,31 +64,6 @@ describe("ProviderSessionStartInput", () => { expect(parsed.providerOptions?.claudeCode?.maxThinkingTokens).toBe(12_000); expect(parsed.runtimeMode).toBe("full-access"); }); - - it("accepts cursor provider payloads", () => { - const parsed = decodeProviderSessionStartInput({ - threadId: "thread-1", - provider: "cursor", - cwd: "/tmp/workspace", - model: "composer-1.5", - modelOptions: { - cursor: { - thinking: true, - }, - }, - providerOptions: { - cursor: { - binaryPath: "/usr/local/bin/agent", - }, - }, - runtimeMode: "approval-required", - }); - expect(parsed.provider).toBe("cursor"); - expect(parsed.model).toBe("composer-1.5"); - expect(parsed.modelOptions?.cursor?.thinking).toBe(true); - expect(parsed.providerOptions?.cursor?.binaryPath).toBe("/usr/local/bin/agent"); - expect(parsed.runtimeMode).toBe("approval-required"); - }); }); describe("ProviderSendTurnInput", () => { diff --git a/packages/contracts/src/provider.ts b/packages/contracts/src/provider.ts index 892127220..8a61f0a15 100644 --- a/packages/contracts/src/provider.ts +++ b/packages/contracts/src/provider.ts @@ -59,14 +59,9 @@ const ClaudeCodeProviderStartOptions = Schema.Struct({ maxThinkingTokens: Schema.optional(NonNegativeInt), }); -const CursorProviderStartOptions = Schema.Struct({ - binaryPath: Schema.optional(TrimmedNonEmptyStringSchema), -}); - export const ProviderStartOptions = Schema.Struct({ codex: Schema.optional(CodexProviderStartOptions), claudeCode: Schema.optional(ClaudeCodeProviderStartOptions), - cursor: Schema.optional(CursorProviderStartOptions), }); export type ProviderStartOptions = typeof ProviderStartOptions.Type; diff --git a/packages/shared/src/model.test.ts b/packages/shared/src/model.test.ts index 785374008..56d541cff 100644 --- a/packages/shared/src/model.test.ts +++ b/packages/shared/src/model.test.ts @@ -1,7 +1,5 @@ import { describe, expect, it } from "vitest"; import { - CURSOR_MODEL_FAMILY_OPTIONS, - CURSOR_REASONING_OPTIONS, DEFAULT_MODEL, DEFAULT_MODEL_BY_PROVIDER, DEFAULT_REASONING_EFFORT_BY_PROVIDER, @@ -13,12 +11,9 @@ import { import { getDefaultModel, getDefaultReasoningEffort, - getCursorModelFamilyOptions, getModelOptions, getReasoningEffortOptions, normalizeModelSlug, - parseCursorModelSelection, - resolveCursorModelFromSelection, resolveModelSlug, resolveModelSlugForProvider, } from "./model"; @@ -49,11 +44,9 @@ describe("normalizeModelSlug", () => { it("uses provider-specific aliases", () => { expect(normalizeModelSlug("sonnet", "claudeCode")).toBe("claude-sonnet-4-6"); expect(normalizeModelSlug("opus-4.6", "claudeCode")).toBe("claude-opus-4-6"); - expect(normalizeModelSlug("claude-haiku-4-5-20251001", "claudeCode")).toBe("claude-haiku-4-5"); - expect(normalizeModelSlug("composer", "cursor")).toBe("composer-1.5"); - expect(normalizeModelSlug("gpt-5.3-codex-spark", "cursor")).toBe("gpt-5.3-codex-spark-preview"); - expect(normalizeModelSlug("gemini-3.1", "cursor")).toBe("gemini-3.1"); - expect(normalizeModelSlug("claude-4.6-sonnet-thinking", "cursor")).toBe("sonnet-4.6-thinking"); + expect(normalizeModelSlug("claude-haiku-4-5-20251001", "claudeCode")).toBe( + "claude-haiku-4-5", + ); }); }); @@ -82,67 +75,12 @@ describe("resolveModelSlug", () => { expect(resolveModelSlugForProvider("claudeCode", "gpt-5.3-codex")).toBe( DEFAULT_MODEL_BY_PROVIDER.claudeCode, ); - expect(resolveModelSlugForProvider("cursor", undefined)).toBe(DEFAULT_MODEL_BY_PROVIDER.cursor); - expect(resolveModelSlugForProvider("cursor", "composer")).toBe("composer-1.5"); - expect(resolveModelSlugForProvider("cursor", "gpt-5.3-codex-high-fast")).toBe( - "gpt-5.3-codex-high-fast", - ); - expect(resolveModelSlugForProvider("cursor", "claude-sonnet-4-6")).toBe( - DEFAULT_MODEL_BY_PROVIDER.cursor, - ); }); it("keeps codex defaults for backward compatibility", () => { expect(getDefaultModel()).toBe(DEFAULT_MODEL); expect(getModelOptions()).toEqual(MODEL_OPTIONS); expect(getModelOptions("claudeCode")).toEqual(MODEL_OPTIONS_BY_PROVIDER.claudeCode); - expect(getModelOptions("cursor")).toEqual(MODEL_OPTIONS_BY_PROVIDER.cursor); - expect(getCursorModelFamilyOptions()).toEqual(CURSOR_MODEL_FAMILY_OPTIONS); - }); -}); - -describe("cursor model selection", () => { - it("includes the expected cursor reasoning levels and families", () => { - expect(CURSOR_REASONING_OPTIONS).toEqual(["low", "normal", "high", "xhigh"]); - expect(getCursorModelFamilyOptions().map((option) => option.slug)).toContain("gpt-5.3-codex"); - expect(getCursorModelFamilyOptions().map((option) => option.slug)).toContain("opus-4.6"); - }); - - it("parses codex reasoning and fast mode variants", () => { - expect(parseCursorModelSelection("gpt-5.3-codex-high-fast")).toEqual({ - family: "gpt-5.3-codex", - reasoning: "high", - fast: true, - thinking: false, - }); - expect(parseCursorModelSelection("gpt-5.2-codex")).toEqual( - parseCursorModelSelection(DEFAULT_MODEL_BY_PROVIDER.cursor), - ); - }); - - it("parses and resolves thinking variants", () => { - expect(parseCursorModelSelection("sonnet-4.6-thinking")).toEqual({ - family: "sonnet-4.6", - reasoning: "normal", - fast: false, - thinking: true, - }); - expect( - resolveCursorModelFromSelection({ - family: "sonnet-4.6", - thinking: true, - }), - ).toBe("sonnet-4.6-thinking"); - }); - - it("resolves codex family selections into concrete model ids", () => { - expect( - resolveCursorModelFromSelection({ - family: "gpt-5.3-codex", - reasoning: "xhigh", - fast: true, - }), - ).toBe("gpt-5.3-codex-xhigh-fast"); }); }); @@ -154,10 +92,6 @@ describe("getReasoningEffortOptions", () => { it("returns no reasoning options for claudeCode", () => { expect(getReasoningEffortOptions("claudeCode")).toEqual([]); }); - - it("returns no reasoning options for cursor", () => { - expect(getReasoningEffortOptions("cursor")).toEqual([]); - }); }); describe("getDefaultReasoningEffort", () => { @@ -166,6 +100,5 @@ describe("getDefaultReasoningEffort", () => { expect(getDefaultReasoningEffort("claudeCode")).toBe( DEFAULT_REASONING_EFFORT_BY_PROVIDER.claudeCode, ); - expect(getDefaultReasoningEffort("cursor")).toBe(DEFAULT_REASONING_EFFORT_BY_PROVIDER.cursor); }); }); diff --git a/packages/shared/src/model.ts b/packages/shared/src/model.ts index 268491a84..30b9e1509 100644 --- a/packages/shared/src/model.ts +++ b/packages/shared/src/model.ts @@ -1,227 +1,24 @@ import { CODEX_REASONING_EFFORT_OPTIONS, - CURSOR_MODEL_FAMILY_OPTIONS, - CURSOR_REASONING_OPTIONS, DEFAULT_MODEL_BY_PROVIDER, DEFAULT_REASONING_EFFORT_BY_PROVIDER, MODEL_OPTIONS_BY_PROVIDER, MODEL_SLUG_ALIASES_BY_PROVIDER, REASONING_EFFORT_OPTIONS_BY_PROVIDER, type CodexReasoningEffort, - type CursorModelFamily, - type CursorModelSlug, - type CursorReasoningOption, type ModelSlug, type ProviderKind, } from "@t3tools/contracts"; -type CursorModelCapability = { - readonly supportsReasoning: boolean; - readonly supportsFast: boolean; - readonly supportsThinking: boolean; - readonly defaultReasoning: CursorReasoningOption; - readonly defaultThinking: boolean; -}; - -const CURSOR_MODEL_CAPABILITY_BY_FAMILY: Record = { - auto: { - supportsReasoning: false, - supportsFast: false, - supportsThinking: false, - defaultReasoning: "normal", - defaultThinking: false, - }, - "composer-1.5": { - supportsReasoning: false, - supportsFast: false, - supportsThinking: false, - defaultReasoning: "normal", - defaultThinking: false, - }, - "composer-1": { - supportsReasoning: false, - supportsFast: false, - supportsThinking: false, - defaultReasoning: "normal", - defaultThinking: false, - }, - "gpt-5.3-codex": { - supportsReasoning: true, - supportsFast: true, - supportsThinking: false, - defaultReasoning: "normal", - defaultThinking: false, - }, - "gpt-5.3-codex-spark-preview": { - supportsReasoning: false, - supportsFast: false, - supportsThinking: false, - defaultReasoning: "normal", - defaultThinking: false, - }, - "opus-4.6": { - supportsReasoning: false, - supportsFast: false, - supportsThinking: true, - defaultReasoning: "normal", - defaultThinking: true, - }, - "opus-4.5": { - supportsReasoning: false, - supportsFast: false, - supportsThinking: true, - defaultReasoning: "normal", - defaultThinking: true, - }, - "sonnet-4.6": { - supportsReasoning: false, - supportsFast: false, - supportsThinking: true, - defaultReasoning: "normal", - defaultThinking: true, - }, - "gemini-3.1-pro": { - supportsReasoning: false, - supportsFast: false, - supportsThinking: false, - defaultReasoning: "normal", - defaultThinking: false, - }, -}; - const MODEL_SLUG_SET_BY_PROVIDER: Record> = { claudeCode: new Set(MODEL_OPTIONS_BY_PROVIDER.claudeCode.map((option) => option.slug)), codex: new Set(MODEL_OPTIONS_BY_PROVIDER.codex.map((option) => option.slug)), - cursor: new Set(MODEL_OPTIONS_BY_PROVIDER.cursor.map((option) => option.slug)), }; -const CURSOR_MODEL_FAMILY_SET = new Set( - CURSOR_MODEL_FAMILY_OPTIONS.map((option) => option.slug), -); - -export interface CursorModelSelection { - readonly family: CursorModelFamily; - readonly reasoning: CursorReasoningOption; - readonly fast: boolean; - readonly thinking: boolean; -} - export function getModelOptions(provider: ProviderKind = "codex") { return MODEL_OPTIONS_BY_PROVIDER[provider]; } -export function getCursorModelFamilyOptions() { - return CURSOR_MODEL_FAMILY_OPTIONS; -} - -export function getCursorModelCapabilities(family: CursorModelFamily) { - return CURSOR_MODEL_CAPABILITY_BY_FAMILY[family]; -} - -function fallbackCursorModelFamily(): CursorModelFamily { - const fallback = parseCursorModelSelection(DEFAULT_MODEL_BY_PROVIDER.cursor); - return fallback.family; -} - -function resolveCursorModelFamily(model: string | null | undefined): CursorModelFamily { - const normalized = normalizeModelSlug(model, "cursor"); - if (!normalized) { - return fallbackCursorModelFamily(); - } - - if ( - normalized === "gpt-5.3-codex" || - normalized === "gpt-5.3-codex-fast" || - normalized === "gpt-5.3-codex-low" || - normalized === "gpt-5.3-codex-low-fast" || - normalized === "gpt-5.3-codex-high" || - normalized === "gpt-5.3-codex-high-fast" || - normalized === "gpt-5.3-codex-xhigh" || - normalized === "gpt-5.3-codex-xhigh-fast" - ) { - return "gpt-5.3-codex"; - } - - if (normalized === "sonnet-4.6-thinking") { - return "sonnet-4.6"; - } - if (normalized === "opus-4.6-thinking") { - return "opus-4.6"; - } - if (normalized === "opus-4.5-thinking") { - return "opus-4.5"; - } - - return CURSOR_MODEL_FAMILY_SET.has(normalized as CursorModelFamily) - ? (normalized as CursorModelFamily) - : fallbackCursorModelFamily(); -} - -function resolveCursorReasoning(model: CursorModelSlug): CursorReasoningOption { - if (model.includes("-xhigh")) return "xhigh"; - if (model.includes("-high")) return "high"; - if (model.includes("-low")) return "low"; - return "normal"; -} - -export function parseCursorModelSelection(model: string | null | undefined): CursorModelSelection { - const family = resolveCursorModelFamily(model); - const capability = CURSOR_MODEL_CAPABILITY_BY_FAMILY[family]; - const normalized = resolveModelSlugForProvider("cursor", model) as CursorModelSlug; - - if (capability.supportsReasoning) { - return { - family, - reasoning: resolveCursorReasoning(normalized), - fast: normalized.endsWith("-fast"), - thinking: false, - }; - } - - if (capability.supportsThinking) { - return { - family, - reasoning: capability.defaultReasoning, - fast: false, - thinking: normalized.endsWith("-thinking"), - }; - } - - return { - family, - reasoning: capability.defaultReasoning, - fast: false, - thinking: capability.defaultThinking, - }; -} - -export function resolveCursorModelFromSelection(input: { - readonly family: CursorModelFamily; - readonly reasoning?: CursorReasoningOption | null; - readonly fast?: boolean; - readonly thinking?: boolean; -}): CursorModelSlug { - const family = resolveCursorModelFamily(input.family); - const capability = CURSOR_MODEL_CAPABILITY_BY_FAMILY[family]; - - if (capability.supportsReasoning) { - const reasoning = CURSOR_REASONING_OPTIONS.includes(input.reasoning ?? "normal") - ? (input.reasoning ?? "normal") - : capability.defaultReasoning; - const reasoningSuffix = reasoning === "normal" ? "" : `-${reasoning}`; - const fastSuffix = input.fast ? "-fast" : ""; - const candidate = `${family}${reasoningSuffix}${fastSuffix}`; - return resolveModelSlugForProvider("cursor", candidate) as CursorModelSlug; - } - - if (capability.supportsThinking) { - const candidate = input.thinking ? `${family}-thinking` : family; - return resolveModelSlugForProvider("cursor", candidate) as CursorModelSlug; - } - - return resolveModelSlugForProvider("cursor", family) as CursorModelSlug; -} - export function getDefaultModel(provider: ProviderKind = "codex"): ModelSlug { return DEFAULT_MODEL_BY_PROVIDER[provider]; } From c5bfff15834bcb36511278a9b709337a565694f3 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 16 Mar 2026 10:41:05 -0700 Subject: [PATCH 12/23] unify start option union --- packages/contracts/src/orchestration.test.ts | 4 ++-- packages/contracts/src/orchestration.ts | 16 ++-------------- packages/contracts/src/provider.ts | 4 ++-- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/packages/contracts/src/orchestration.test.ts b/packages/contracts/src/orchestration.test.ts index f98bca262..25a641edb 100644 --- a/packages/contracts/src/orchestration.test.ts +++ b/packages/contracts/src/orchestration.test.ts @@ -130,11 +130,11 @@ it.effect("preserves explicit provider and runtime mode in thread.turn.start", ( text: "hello", attachments: [], }, - provider: "claudeCode", + provider: "codex", runtimeMode: "full-access", createdAt: "2026-01-01T00:00:00.000Z", }); - assert.strictEqual(parsed.provider, "claudeCode"); + assert.strictEqual(parsed.provider, "codex"); assert.strictEqual(parsed.runtimeMode, "full-access"); assert.strictEqual(parsed.interactionMode, DEFAULT_PROVIDER_INTERACTION_MODE); }), diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 95605306e..c7bff3fb0 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -14,6 +14,7 @@ import { TrimmedNonEmptyString, TurnId, } from "./baseSchemas"; +import { ProviderStartOptions } from "./provider"; export const ORCHESTRATION_WS_METHODS = { getSnapshot: "orchestration.getSnapshot", @@ -43,20 +44,7 @@ export const ProviderSandboxMode = Schema.Literals([ ]); export type ProviderSandboxMode = typeof ProviderSandboxMode.Type; export const DEFAULT_PROVIDER_KIND: ProviderKind = "codex"; -const CodexProviderStartOptions = Schema.Struct({ - binaryPath: Schema.optional(TrimmedNonEmptyString), - homePath: Schema.optional(TrimmedNonEmptyString), -}); -const ProviderStartOptions = Schema.Struct({ - codex: Schema.optional(CodexProviderStartOptions), - claudeCode: Schema.optional( - Schema.Struct({ - binaryPath: Schema.optional(TrimmedNonEmptyString), - permissionMode: Schema.optional(TrimmedNonEmptyString), - maxThinkingTokens: Schema.optional(NonNegativeInt), - }), - ), -}); + export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]); export type RuntimeMode = typeof RuntimeMode.Type; export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; diff --git a/packages/contracts/src/provider.ts b/packages/contracts/src/provider.ts index 8a61f0a15..c04854a48 100644 --- a/packages/contracts/src/provider.ts +++ b/packages/contracts/src/provider.ts @@ -48,12 +48,12 @@ export const ProviderSession = Schema.Struct({ }); export type ProviderSession = typeof ProviderSession.Type; -const CodexProviderStartOptions = Schema.Struct({ +export const CodexProviderStartOptions = Schema.Struct({ binaryPath: Schema.optional(TrimmedNonEmptyStringSchema), homePath: Schema.optional(TrimmedNonEmptyStringSchema), }); -const ClaudeCodeProviderStartOptions = Schema.Struct({ +export const ClaudeCodeProviderStartOptions = Schema.Struct({ binaryPath: Schema.optional(TrimmedNonEmptyStringSchema), permissionMode: Schema.optional(TrimmedNonEmptyStringSchema), maxThinkingTokens: Schema.optional(NonNegativeInt), From 73152abe81f305448127db52ea9e0687c217f31e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 16 Mar 2026 11:03:31 -0700 Subject: [PATCH 13/23] fmt --- .../server/src/orchestration/Layers/ProviderCommandReactor.ts | 3 +-- packages/shared/src/model.test.ts | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index 5a52d41be..49e1ca917 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -206,8 +206,7 @@ const make = Effect.gen(function* () { const desiredRuntimeMode = thread.runtimeMode; const currentProvider: ProviderKind | undefined = - thread.session?.providerName === "codex" || - thread.session?.providerName === "claudeCode" + thread.session?.providerName === "codex" || thread.session?.providerName === "claudeCode" ? thread.session.providerName : undefined; const preferredProvider: ProviderKind | undefined = options?.provider ?? currentProvider; diff --git a/packages/shared/src/model.test.ts b/packages/shared/src/model.test.ts index 56d541cff..eb3760248 100644 --- a/packages/shared/src/model.test.ts +++ b/packages/shared/src/model.test.ts @@ -44,9 +44,7 @@ describe("normalizeModelSlug", () => { it("uses provider-specific aliases", () => { expect(normalizeModelSlug("sonnet", "claudeCode")).toBe("claude-sonnet-4-6"); expect(normalizeModelSlug("opus-4.6", "claudeCode")).toBe("claude-opus-4-6"); - expect(normalizeModelSlug("claude-haiku-4-5-20251001", "claudeCode")).toBe( - "claude-haiku-4-5", - ); + expect(normalizeModelSlug("claude-haiku-4-5-20251001", "claudeCode")).toBe("claude-haiku-4-5"); }); }); From e0facb03bde3ac486b374ae6ffa6c28d8183bc43 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 16 Mar 2026 11:06:26 -0700 Subject: [PATCH 14/23] fix circular import --- packages/contracts/src/orchestration.ts | 18 +++++++++++- packages/contracts/src/provider.ts | 38 +++++++------------------ 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index c7bff3fb0..557aa8159 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -14,7 +14,6 @@ import { TrimmedNonEmptyString, TurnId, } from "./baseSchemas"; -import { ProviderStartOptions } from "./provider"; export const ORCHESTRATION_WS_METHODS = { getSnapshot: "orchestration.getSnapshot", @@ -45,6 +44,23 @@ export const ProviderSandboxMode = Schema.Literals([ export type ProviderSandboxMode = typeof ProviderSandboxMode.Type; export const DEFAULT_PROVIDER_KIND: ProviderKind = "codex"; +export const CodexProviderStartOptions = Schema.Struct({ + binaryPath: Schema.optional(TrimmedNonEmptyString), + homePath: Schema.optional(TrimmedNonEmptyString), +}); + +export const ClaudeCodeProviderStartOptions = Schema.Struct({ + binaryPath: Schema.optional(TrimmedNonEmptyString), + permissionMode: Schema.optional(TrimmedNonEmptyString), + maxThinkingTokens: Schema.optional(NonNegativeInt), +}); + +export const ProviderStartOptions = Schema.Struct({ + codex: Schema.optional(CodexProviderStartOptions), + claudeCode: Schema.optional(ClaudeCodeProviderStartOptions), +}); +export type ProviderStartOptions = typeof ProviderStartOptions.Type; + export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]); export type RuntimeMode = typeof RuntimeMode.Type; export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; diff --git a/packages/contracts/src/provider.ts b/packages/contracts/src/provider.ts index c04854a48..db8e24954 100644 --- a/packages/contracts/src/provider.ts +++ b/packages/contracts/src/provider.ts @@ -5,7 +5,6 @@ import { ApprovalRequestId, EventId, IsoDateTime, - NonNegativeInt, ProviderItemId, ThreadId, TurnId, @@ -20,11 +19,11 @@ import { ProviderKind, ProviderRequestKind, ProviderSandboxMode, + ProviderStartOptions, ProviderUserInputAnswers, RuntimeMode, } from "./orchestration"; -const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString; const ProviderSessionStatus = Schema.Literals([ "connecting", "ready", @@ -37,39 +36,22 @@ export const ProviderSession = Schema.Struct({ provider: ProviderKind, status: ProviderSessionStatus, runtimeMode: RuntimeMode, - cwd: Schema.optional(TrimmedNonEmptyStringSchema), - model: Schema.optional(TrimmedNonEmptyStringSchema), + cwd: Schema.optional(TrimmedNonEmptyString), + model: Schema.optional(TrimmedNonEmptyString), threadId: ThreadId, resumeCursor: Schema.optional(Schema.Unknown), activeTurnId: Schema.optional(TurnId), createdAt: IsoDateTime, updatedAt: IsoDateTime, - lastError: Schema.optional(TrimmedNonEmptyStringSchema), + lastError: Schema.optional(TrimmedNonEmptyString), }); export type ProviderSession = typeof ProviderSession.Type; -export const CodexProviderStartOptions = Schema.Struct({ - binaryPath: Schema.optional(TrimmedNonEmptyStringSchema), - homePath: Schema.optional(TrimmedNonEmptyStringSchema), -}); - -export const ClaudeCodeProviderStartOptions = Schema.Struct({ - binaryPath: Schema.optional(TrimmedNonEmptyStringSchema), - permissionMode: Schema.optional(TrimmedNonEmptyStringSchema), - maxThinkingTokens: Schema.optional(NonNegativeInt), -}); - -export const ProviderStartOptions = Schema.Struct({ - codex: Schema.optional(CodexProviderStartOptions), - claudeCode: Schema.optional(ClaudeCodeProviderStartOptions), -}); -export type ProviderStartOptions = typeof ProviderStartOptions.Type; - export const ProviderSessionStartInput = Schema.Struct({ threadId: ThreadId, provider: Schema.optional(ProviderKind), - cwd: Schema.optional(TrimmedNonEmptyStringSchema), - model: Schema.optional(TrimmedNonEmptyStringSchema), + cwd: Schema.optional(TrimmedNonEmptyString), + model: Schema.optional(TrimmedNonEmptyString), modelOptions: Schema.optional(ProviderModelOptions), resumeCursor: Schema.optional(Schema.Unknown), approvalPolicy: Schema.optional(ProviderApprovalPolicy), @@ -82,12 +64,12 @@ export type ProviderSessionStartInput = typeof ProviderSessionStartInput.Type; export const ProviderSendTurnInput = Schema.Struct({ threadId: ThreadId, input: Schema.optional( - TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(PROVIDER_SEND_TURN_MAX_INPUT_CHARS)), + TrimmedNonEmptyString.check(Schema.isMaxLength(PROVIDER_SEND_TURN_MAX_INPUT_CHARS)), ), attachments: Schema.optional( Schema.Array(ChatAttachment).check(Schema.isMaxLength(PROVIDER_SEND_TURN_MAX_ATTACHMENTS)), ), - model: Schema.optional(TrimmedNonEmptyStringSchema), + model: Schema.optional(TrimmedNonEmptyString), modelOptions: Schema.optional(ProviderModelOptions), interactionMode: Schema.optional(ProviderInteractionMode), }); @@ -133,8 +115,8 @@ export const ProviderEvent = Schema.Struct({ provider: ProviderKind, threadId: ThreadId, createdAt: IsoDateTime, - method: TrimmedNonEmptyStringSchema, - message: Schema.optional(TrimmedNonEmptyStringSchema), + method: TrimmedNonEmptyString, + message: Schema.optional(TrimmedNonEmptyString), turnId: Schema.optional(TurnId), itemId: Schema.optional(ProviderItemId), requestId: Schema.optional(ApprovalRequestId), From d2b00e655008fdb9b7f8c474545ec49c23520657 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 16 Mar 2026 11:11:57 -0700 Subject: [PATCH 15/23] Show Claude Code in provider picker and add Cursor placeholder - Include Claude Code in available provider options in the picker - Add Cursor as a disabled placeholder provider with icon mapping - Update session-logic tests to assert provider availability and ordering --- apps/web/src/components/chat/ProviderModelPicker.tsx | 10 ++++------ apps/web/src/session-logic.test.ts | 9 ++++++++- apps/web/src/session-logic.ts | 3 ++- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/chat/ProviderModelPicker.tsx b/apps/web/src/components/chat/ProviderModelPicker.tsx index a15f84e18..837ceced3 100644 --- a/apps/web/src/components/chat/ProviderModelPicker.tsx +++ b/apps/web/src/components/chat/ProviderModelPicker.tsx @@ -17,7 +17,7 @@ import { MenuSubTrigger, MenuTrigger, } from "../ui/menu"; -import { ClaudeAI, Gemini, Icon, OpenAI, OpenCodeIcon } from "../Icons"; +import { ClaudeAI, CursorIcon, Gemini, Icon, OpenAI, OpenCodeIcon } from "../Icons"; import { cn } from "~/lib/utils"; function isAvailableProviderOption(option: (typeof PROVIDER_OPTIONS)[number]): option is { @@ -25,7 +25,7 @@ function isAvailableProviderOption(option: (typeof PROVIDER_OPTIONS)[number]): o label: string; available: true; } { - return option.available && option.value !== "claudeCode"; + return option.available; } function resolveModelForProviderPicker( @@ -64,6 +64,7 @@ function resolveModelForProviderPicker( const PROVIDER_ICON_BY_PROVIDER: Record = { codex: OpenAI, claudeCode: ClaudeAI, + cursor: CursorIcon, }; export const AVAILABLE_PROVIDER_OPTIONS = PROVIDER_OPTIONS.filter(isAvailableProviderOption); @@ -174,10 +175,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {