From 6a10e97a97f768137f7978375e4cfa13211b063e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 28 Feb 2026 11:28:54 -0800 Subject: [PATCH] Align Claude permissions and provider-specific reasoning effort - Default Claude sessions to bypass permissions when approval policy is `never`, while preserving explicit `permissionMode` - Hide/send reasoning effort only for providers that support it in ChatView - Add coverage for Claude permission-mode derivation and precedence, and update runtime event model docs --- EVENTS.md | 520 ++++++++++++++++++ .../provider/Layers/ClaudeCodeAdapter.test.ts | 42 ++ .../src/provider/Layers/ClaudeCodeAdapter.ts | 4 +- apps/web/src/components/ChatView.tsx | 24 +- packages/contracts/src/model.test.ts | 12 + packages/contracts/src/model.ts | 16 +- 6 files changed, 608 insertions(+), 10 deletions(-) create mode 100644 EVENTS.md diff --git a/EVENTS.md b/EVENTS.md new file mode 100644 index 0000000000000000000000000000000000000000..a169a4bce7d7e14850968b17222d266bcdb6c3ab --- /dev/null +++ b/EVENTS.md @@ -0,0 +1,520 @@ +# EVENTS.md + +## Goal + +Define a **breaking** canonical provider-runtime event model that can represent the full useful surface of: + +- Claude Agent SDK types (`@anthropic-ai/claude-agent-sdk@0.2.62`) +- Codex App Server protocol (`schema/json/*`, including `ServerNotification`, `ServerRequest`, `EventMsg`, and v2 payload schemas) +- Codex TypeScript SDK thread events/items (`sdk/typescript/src/events.ts`, `items.ts`) + +This is a mapping/spec document only (no downstream compatibility constraints). + +--- + +## 1) Proposed Canonical Runtime Event Model (V2) + +### 1.1 Envelope + +```ts +type RuntimeEventRaw = { + source: + | "codex.app-server.notification" + | "codex.app-server.request" + | "codex.eventmsg" + | "claude.sdk.message" + | "claude.sdk.permission" + | "codex.sdk.thread-event"; + method?: string; + messageType?: string; + payload: unknown; +}; + +// Add these to `packages/contracts/src/baseSchemas.ts` for V2. +export const RuntimeItemId = makeEntityId("RuntimeItemId"); +export type RuntimeItemId = typeof RuntimeItemId.Type; +export const RuntimeRequestId = makeEntityId("RuntimeRequestId"); +export type RuntimeRequestId = typeof RuntimeRequestId.Type; +export const RuntimeTaskId = makeEntityId("RuntimeTaskId"); +export type RuntimeTaskId = typeof RuntimeTaskId.Type; +export const RuntimeSessionId = makeEntityId("RuntimeSessionId"); +export type RuntimeSessionId = typeof RuntimeSessionId.Type; + +type RuntimeEventBase = { + eventId: EventId; + provider: ProviderKind; + sessionId: RuntimeSessionId; + createdAt: IsoDateTime; + + // Canonical T3 IDs (not provider-native IDs). + threadId?: ThreadId; + turnId?: TurnId; + itemId?: RuntimeItemId; + requestId?: RuntimeRequestId; + + // Provider-native IDs preserved for correlation/debugging. + providerRefs?: { + providerSessionId?: ProviderSessionId; + providerThreadId?: ProviderThreadId; + providerTurnId?: ProviderTurnId; + providerItemId?: ProviderItemId; + providerRequestId?: TrimmedNonEmptyString; + }; + + type: TType; + payload: TPayload; + raw?: RuntimeEventRaw; +}; +``` + +### 1.2 Canonical Event Families + +```ts +type CanonicalRuntimeEventType = + // lifecycle + | "session.started" + | "session.configured" + | "session.state.changed" + | "session.exited" + | "thread.started" + | "thread.state.changed" + | "thread.metadata.updated" + | "thread.token-usage.updated" + | "thread.realtime.started" + | "thread.realtime.item-added" + | "thread.realtime.audio.delta" + | "thread.realtime.error" + | "thread.realtime.closed" + | "turn.started" + | "turn.completed" + | "turn.aborted" + | "turn.plan.updated" + | "turn.diff.updated" + + // item + content stream + | "item.started" + | "item.updated" + | "item.completed" + | "content.delta" + + // approvals / input / tool-protocol requests + | "request.opened" + | "request.resolved" + | "user-input.requested" + | "user-input.resolved" + + // provider/system telemetry + | "task.started" + | "task.progress" + | "task.completed" + | "hook.started" + | "hook.progress" + | "hook.completed" + | "tool.progress" + | "tool.summary" + | "auth.status" + | "account.updated" + | "account.rate-limits.updated" + | "mcp.status.updated" + | "mcp.oauth.completed" + | "model.rerouted" + | "config.warning" + | "deprecation.notice" + | "files.persisted" + | "runtime.warning" + | "runtime.error"; +``` + +### 1.3 Canonical Item Type + +```ts +type CanonicalItemType = + | "user_message" + | "assistant_message" + | "reasoning" + | "plan" + | "command_execution" + | "file_change" + | "mcp_tool_call" + | "dynamic_tool_call" + | "collab_agent_tool_call" + | "web_search" + | "image_view" + | "review_entered" + | "review_exited" + | "context_compaction" + | "error" + | "unknown"; +``` + +### 1.4 Canonical Request Type + +```ts +type CanonicalRequestType = + | "command_execution_approval" + | "file_change_approval" + | "apply_patch_approval" + | "exec_command_approval" + | "tool_user_input" + | "dynamic_tool_call" + | "auth_tokens_refresh" + | "unknown"; +``` + +### 1.5 Discriminated Union (Type-Safe) + +```ts +type SessionState = "starting" | "ready" | "running" | "waiting" | "stopped" | "error"; +type ThreadState = "active" | "idle" | "archived" | "closed" | "compacted" | "error"; +type TurnState = "completed" | "failed" | "interrupted" | "cancelled"; + +type ContentStreamKind = + | "assistant_text" + | "reasoning_text" + | "reasoning_summary_text" + | "plan_text" + | "command_output" + | "file_change_output" + | "unknown"; + +type ItemLifecyclePayload = { + itemType: CanonicalItemType; + status?: "inProgress" | "completed" | "failed" | "declined"; + title?: string; + detail?: string; + data?: unknown; +}; + +type RuntimeEventPayloadByType = { + "session.started": { message?: string; resume?: unknown }; + "session.configured": { config: Record }; + "session.state.changed": { state: SessionState; reason?: string; detail?: string }; + "session.exited": { reason?: string; recoverable?: boolean; exitKind?: "graceful" | "error" }; + + "thread.started": { providerThreadId?: ProviderThreadId }; + "thread.state.changed": { state: ThreadState; detail?: unknown }; + "thread.metadata.updated": { name?: string; metadata?: Record }; + "thread.token-usage.updated": { usage: unknown }; + "thread.realtime.started": { realtimeSessionId?: string }; + "thread.realtime.item-added": { item: unknown }; + "thread.realtime.audio.delta": { audio: unknown }; + "thread.realtime.error": { message: string }; + "thread.realtime.closed": { reason?: string }; + + "turn.started": { model?: string; effort?: string }; + "turn.completed": { + state: TurnState; + stopReason?: string | null; + usage?: unknown; + modelUsage?: Record; + totalCostUsd?: number; + errorMessage?: string; + }; + "turn.aborted": { reason: string }; + "turn.plan.updated": { + explanation?: string | null; + plan: Array<{ step: string; status: "pending" | "inProgress" | "completed" }>; + }; + "turn.diff.updated": { unifiedDiff: string }; + + "item.started": ItemLifecyclePayload; + "item.updated": ItemLifecyclePayload; + "item.completed": ItemLifecyclePayload; + "content.delta": { + streamKind: ContentStreamKind; + delta: string; + contentIndex?: number; + summaryIndex?: number; + }; + + "request.opened": { + requestType: CanonicalRequestType; + detail?: string; + args?: unknown; + }; + "request.resolved": { + requestType: CanonicalRequestType; + decision?: string; + resolution?: unknown; + }; + "user-input.requested": { questions: unknown }; + "user-input.resolved": { answers: unknown }; + + "task.started": { taskId: RuntimeTaskId; description?: string; taskType?: string }; + "task.progress": { + taskId: RuntimeTaskId; + description: string; + usage?: unknown; + lastToolName?: string; + }; + "task.completed": { + taskId: RuntimeTaskId; + status: "completed" | "failed" | "stopped"; + summary?: string; + usage?: unknown; + }; + "hook.started": { hookId: string; hookName: string; hookEvent: string }; + "hook.progress": { hookId: string; output?: string; stdout?: string; stderr?: string }; + "hook.completed": { + hookId: string; + outcome: "success" | "error" | "cancelled"; + output?: string; + stdout?: string; + stderr?: string; + exitCode?: number; + }; + "tool.progress": { + toolUseId?: string; + toolName?: string; + summary?: string; + elapsedSeconds?: number; + }; + "tool.summary": { summary: string; precedingToolUseIds?: string[] }; + "auth.status": { isAuthenticating?: boolean; output?: string[]; error?: string }; + "account.updated": { account: unknown }; + "account.rate-limits.updated": { rateLimits: unknown }; + "mcp.status.updated": { status: unknown }; + "mcp.oauth.completed": { success: boolean; name?: string; error?: string }; + "model.rerouted": { fromModel: string; toModel: string; reason: string }; + "config.warning": { summary: string; details?: string; path?: string; range?: unknown }; + "deprecation.notice": { summary: string; details?: string }; + "files.persisted": { + files: Array<{ filename: string; fileId: string }>; + failed?: Array<{ filename: string; error: string }>; + }; + "runtime.warning": { message: string; detail?: unknown }; + "runtime.error": { + message: string; + class?: + | "provider_error" + | "transport_error" + | "permission_error" + | "validation_error" + | "unknown"; + detail?: unknown; + }; +}; + +type CanonicalRuntimeEventTypeFromPayloadMap = keyof RuntimeEventPayloadByType; +type MissingPayloadTypes = Exclude< + CanonicalRuntimeEventType, + CanonicalRuntimeEventTypeFromPayloadMap +>; +type ExtraPayloadTypes = Exclude< + CanonicalRuntimeEventTypeFromPayloadMap, + CanonicalRuntimeEventType +>; +// Both should be `never`. + +export type ProviderRuntimeEventV2 = { + [TType in CanonicalRuntimeEventTypeFromPayloadMap]: RuntimeEventBase< + TType, + RuntimeEventPayloadByType[TType] + >; +}[CanonicalRuntimeEventTypeFromPayloadMap]; +``` + +### 1.6 ID Semantics + +- `eventId`, `sessionId`, `threadId`, `turnId`, `itemId`, `requestId` are **canonical T3 IDs** (branded). +- Provider IDs must go in `providerRefs.*` (and `raw`), never in canonical fields. +- If a canonical id is not known at emit time, omit it and emit only `providerRefs` + `raw`. + +--- + +## 2) Changes vs Current `ProviderRuntimeEvent` (packages/contracts/src/providerRuntime.ts) + +## 2.1 Update Existing + +| Current | Action | +| ----------------- | -------------------------------------------------------------------------------------- | +| `session.started` | Keep, add structured payload (`resume/fork/config snapshot if known`) | +| `session.exited` | Keep, add `reason`, `exitKind`, `recoverable` | +| `thread.started` | Keep, add source metadata | +| `turn.started` | Keep, include optional model/provider state | +| `turn.completed` | Keep, expand status + usage + stopReason + cost/modelUsage | +| `message.delta` | Replace semantics with generic `content.delta` (message delta becomes one stream kind) | +| `runtime.error` | Keep but include normalized error class + provider raw error info | + +## 2.2 Delete (replaced) + +| Current | Replacement | +| --------------------- | ------------------------------------------------------------------------------------------------------ | +| `message.completed` | `item.completed` for `assistant_message` | +| `tool.started` | `item.started` (`command_execution`, `file_change`, `mcp_tool_call`, `dynamic_tool_call`, etc.) | +| `tool.completed` | `item.completed` | +| `approval.requested` | `request.opened` | +| `approval.resolved` | `request.resolved` | +| `checkpoint.captured` | Remove from provider runtime canonical set (keep checkpoint as orchestration-level projection concern) | + +## 2.3 Create New + +`session.configured`, `session.state.changed`, `thread.state.changed`, `thread.metadata.updated`, `thread.token-usage.updated`, `turn.aborted`, `turn.plan.updated`, `turn.diff.updated`, `item.started`, `item.updated`, `item.completed`, `content.delta`, `request.opened`, `request.resolved`, `user-input.requested`, `user-input.resolved`, `task.*`, `hook.*`, `tool.progress`, `tool.summary`, `auth.status`, `account.*`, `mcp.*`, `model.rerouted`, `config.warning`, `deprecation.notice`, `files.persisted`, realtime thread events. + +--- + +## 3) Codex App Server -> Canonical Mapping + +### 3.1 Server Notifications (`ServerNotification.method`) + +| Codex method | Canonical V2 | Notes | +| --------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------ | +| `thread/started` | `thread.started` | `thread.id -> threadId` | +| `thread/status/changed` | `thread.state.changed` | preserve full status object (`notLoaded/idle/systemError/active{flags}`) | +| `thread/archived` / `thread/unarchived` / `thread/closed` | `thread.state.changed` | normalize `state=archived/unarchived/closed` | +| `thread/name/updated` | `thread.metadata.updated` | `threadName` | +| `thread/tokenUsage/updated` | `thread.token-usage.updated` | preserve totals + breakdown | +| `turn/started` | `turn.started` | `turn.id`, initial turn item snapshot | +| `turn/completed` | `turn.completed` | map `TurnStatus`: `completed/interrupted/failed/inProgress` | +| `turn/diff/updated` | `turn.diff.updated` | keep unified diff payload | +| `turn/plan/updated` | `turn.plan.updated` | preserve `explanation`, steps (`pending/inProgress/completed`) | +| `item/started` | `item.started` | map item type to canonical item type | +| `item/completed` | `item.completed` | map item type + terminal status | +| `item/agentMessage/delta` | `content.delta` | `streamKind=assistant_text` | +| `item/plan/delta` | `content.delta` | `streamKind=plan_text` | +| `item/commandExecution/outputDelta` | `content.delta` | `streamKind=command_output` | +| `item/fileChange/outputDelta` | `content.delta` | `streamKind=file_change_output` | +| `item/reasoning/summaryTextDelta` | `content.delta` | `streamKind=reasoning_summary_text`, `summaryIndex` | +| `item/reasoning/summaryPartAdded` | `item.updated` | item reasoning summary segment boundary | +| `item/reasoning/textDelta` | `content.delta` | `streamKind=reasoning_text`, `contentIndex` | +| `item/mcpToolCall/progress` | `tool.progress` | for MCP calls | +| `item/commandExecution/terminalInteraction` | `item.updated` | interaction input on command execution item | +| `serverRequest/resolved` | `request.resolved` | correlate by request id | +| `model/rerouted` | `model.rerouted` | preserve `fromModel/toModel/reason` | +| `thread/compacted` | `thread.state.changed` | compacted context marker | +| `thread/realtime/started` | `thread.realtime.started` | realtime transport/session id | +| `thread/realtime/itemAdded` | `thread.realtime.item-added` | raw realtime item | +| `thread/realtime/outputAudio/delta` | `thread.realtime.audio.delta` | preserve sample metadata | +| `thread/realtime/error` | `thread.realtime.error` | message | +| `thread/realtime/closed` | `thread.realtime.closed` | optional reason | +| `error` | `runtime.error` | preserve `willRetry`, codex error info | +| `deprecationNotice` | `deprecation.notice` | summary/details | +| `configWarning` | `config.warning` | summary/details/path/range | +| `account/updated` | `account.updated` | auth mode updates | +| `account/rateLimits/updated` | `account.rate-limits.updated` | rate limits snapshot | +| `mcpServer/oauthLogin/completed` | `mcp.oauth.completed` | success/error/name | +| `windows/worldWritableWarning` | `runtime.warning` | platform warning | +| `windowsSandbox/setupCompleted` | `session.state.changed` + `runtime.warning` | success/failure diagnostic | +| `app/list/updated` | `runtime.warning` or future `app-list.updated` | optional if UI wants app ecosystem surface | +| `account/login/completed` | `auth.status` | login success/error | + +### 3.2 Server Requests (`ServerRequest.method`) + +| Codex request method | Canonical V2 | +| --------------------------------------- | ------------------------------------------------------------------------- | +| `item/commandExecution/requestApproval` | `request.opened` (`requestType=command_execution_approval`) | +| `item/fileChange/requestApproval` | `request.opened` (`requestType=file_change_approval`) | +| `applyPatchApproval` | `request.opened` (`requestType=apply_patch_approval`) | +| `execCommandApproval` | `request.opened` (`requestType=exec_command_approval`) | +| `item/tool/requestUserInput` | `user-input.requested` + `request.opened` (`requestType=tool_user_input`) | +| `item/tool/call` | `request.opened` (`requestType=dynamic_tool_call`) | +| `account/chatgptAuthTokens/refresh` | `request.opened` (`requestType=auth_tokens_refresh`) | + +### 3.3 EventMsg cross-reference (`EventMsg.type`) + +EventMsg is broader than server notification coverage. Important equivalents: + +- `task_started` / `task_complete` -> `turn.started` / `turn.completed` +- `agent_message_delta`, `agent_message_content_delta` -> `content.delta (assistant_text)` +- `agent_reasoning_delta`, `reasoning_content_delta`, `reasoning_raw_content_delta` -> `content.delta (reasoning*)` +- `exec_command_begin/output_delta/end` -> `item.started/content.delta/item.completed` (`command_execution`) +- `patch_apply_begin/end` -> `item.updated/item.completed` (`file_change`) +- `exec_approval_request`, `apply_patch_approval_request`, `request_user_input` -> `request.opened` / `user-input.requested` +- `plan_update`, `plan_delta` -> `turn.plan.updated` / `content.delta(plan_text)` +- `turn_diff`, `turn_aborted`, `thread_rolled_back`, `context_compacted`, `model_reroute` -> dedicated `turn.*` / `thread.*` / `model.rerouted` +- collab events (`collab_*`) -> `item.*` (`collab_agent_tool_call`) or `task.*` depending UI preference + +--- + +## 4) Claude Agent SDK -> Canonical Mapping + +### 4.1 SDKMessage Mapping (`@anthropic-ai/claude-agent-sdk@0.2.62`) + +| Claude SDK message | Canonical V2 | Notes | +| ------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `assistant` (`SDKAssistantMessage`) | `item.updated` or `item.completed` (`assistant_message`) | preserve `message`, `uuid`, optional `error` | +| `stream_event` (`SDKPartialAssistantMessage`) `content_block_delta[text_delta]` | `content.delta` (`assistant_text`) | primary streamed assistant text | +| `stream_event` `content_block_start[tool_use/server_tool_use/mcp_tool_use]` | `item.started` | map tool name -> canonical item type (`command_execution`/`file_change`/`mcp_tool_call`/`dynamic_tool_call`) | +| `stream_event` `content_block_stop` | `item.completed` | close tool item started above | +| `result` `subtype=success` | `turn.completed` (`status=completed`) | include usage/modelUsage/cost/stop_reason | +| `result` `subtype=error_*` | `runtime.error` + `turn.completed(status=failed)` | include error list + subtype | +| `system:init` | `session.configured` | model/tools/permissionMode/skills/plugins/mcp status | +| `system:status` | `session.state.changed` | compacting status + permission mode | +| `system:compact_boundary` | `thread.state.changed` | context compaction boundary | +| `system:hook_started` | `hook.started` | hook telemetry | +| `system:hook_progress` | `hook.progress` | hook stdout/stderr/output | +| `system:hook_response` | `hook.completed` | outcome + exit_code | +| `tool_progress` | `tool.progress` | elapsed seconds, tool name, tool_use_id | +| `tool_use_summary` | `tool.summary` | summary + related tool use ids | +| `auth_status` | `auth.status` | authentication progress/errors | +| `system:task_started` | `task.started` | background task/subagent activity | +| `system:task_progress` | `task.progress` | incremental background task updates | +| `system:task_notification` | `task.completed` | status completed/failed/stopped | +| `system:files_persisted` | `files.persisted` | file ids persisted to backend | +| `user` / `user (isReplay)` | optional `item.started/item.completed (user_message)` | keep if we want full transcript parity | +| `rate_limit` / `prompt_suggestion` (in union) | `account.rate-limits.updated` or `item.updated(plan_suggestion)` | preserve raw payload even if sparse typing | + +### 4.2 Claude Permission Callback Mapping (`canUseTool`) + +| Claude callback phase | Canonical V2 | +| ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| permission request (`toolName`, input, suggestions, blockedPath, decisionReason, toolUseID) | `request.opened` (`requestType=command_execution_approval` or `file_change_approval` or `unknown`) | +| user decision allow/deny (+updatedPermissions) | `request.resolved` | +| interrupt/deny behavior | `turn.completed(status=interrupted)` or `runtime.error` as appropriate | + +--- + +## 5) Codex SDK Thread Events -> Canonical Mapping + +(For parity with direct Codex SDK integrations and to cross-reference app-server behavior.) + +| Codex SDK ThreadEvent | Canonical V2 | +| --------------------- | ------------------------------------------------- | +| `thread.started` | `thread.started` | +| `turn.started` | `turn.started` | +| `turn.completed` | `turn.completed` | +| `turn.failed` | `runtime.error` + `turn.completed(status=failed)` | +| `item.started` | `item.started` | +| `item.updated` | `item.updated` | +| `item.completed` | `item.completed` | +| `error` | `runtime.error` | + +Codex SDK `ThreadItem.type` (`agent_message`, `reasoning`, `command_execution`, `file_change`, `mcp_tool_call`, `web_search`, `todo_list`, `error`) maps directly to canonical item types (`assistant_message`, `reasoning`, `command_execution`, `file_change`, `mcp_tool_call`, `web_search`, `plan`, `error`). + +--- + +## 6) Cross-Provider Equivalence (Claude <-> Codex) + +| Concept | Codex source | Claude source | Canonical | +| --------------------------- | -------------------------------------------- | ----------------------------------------------------------- | ------------------------------- | +| Turn start | `turn/started` | first active response turn + `sendTurn` start | `turn.started` | +| Turn completion | `turn/completed` | `result` | `turn.completed` | +| Assistant text streaming | `item/agentMessage/delta` | `stream_event content_block_delta[text_delta]` | `content.delta(assistant_text)` | +| Tool call start | `item/started` with tool item | `content_block_start tool_use/server_tool_use/mcp_tool_use` | `item.started` | +| Tool call end | `item/completed` with tool item | `content_block_stop` | `item.completed` | +| Approval required | server request `.../requestApproval` | `canUseTool` callback | `request.opened` | +| Approval resolved | `serverRequest/resolved` + response decision | callback resolution decision | `request.resolved` | +| Runtime error | `error` notification / failed turn | `result error_*` or assistant error | `runtime.error` | +| Model reroute | `model/rerouted` | (none native, but could appear as result/meta) | `model.rerouted` | +| Session capabilities/config | `session_configured`/init responses | `system:init` | `session.configured` | + +--- + +## 7) Implementation Guidance + +1. Emit one canonical event per source message minimum; emit additional derived events when needed (for example `runtime.error` + `turn.completed`). +2. Always attach `raw` payload so provider-specific detail is never lost. +3. Replace current Codex adapter heuristic mapping (`tool.*`, `approval.*`, `message.*`) with method/item-driven mapping from app-server schemas. +4. Extend Claude adapter to emit non-chat telemetry (`system:*`, `task_*`, `hook_*`, `tool_progress`, `auth_status`, `files_persisted`) into canonical events. +5. Keep ID correlation deterministic: + - request lifecycle keyed by `requestId` / approval id / tool use id + - item lifecycle keyed by `itemId` (synthesize stable ids for Claude when missing) +6. Treat unknown provider events as `runtime.warning` + raw passthrough instead of dropping. + +--- + +## 8) Summary of Breaking Schema Direction + +- Move from a small, chat-centric runtime union to an **item/lifecycle/protocol-complete canonical runtime model**. +- Normalize Claude and Codex onto the same event families (`turn`, `item`, `content`, `request`, `task`, `hook`, `state`), keeping provider specifics in `payload` + `raw`. +- This supports richer integration ports without provider-specific logic in downstream consumers. diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts index 21bf2376e07121eb9c4a6b6a0ad6f2db518b7f22..b92cdec8f24b27509b4b86f7fa2d270ee9b136d7 100644 --- a/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts @@ -182,6 +182,48 @@ describe("ClaudeCodeAdapterLive", () => { ); }); + 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({ + provider: "claudeCode", + approvalPolicy: "never", + sandboxMode: "danger-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({ + provider: "claudeCode", + approvalPolicy: "never", + 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* () { diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts index be42594ed0464e1866c4bbe6947d92d7410539c1..989cb73b1be21607dfd1a9b6938b78fa62d9736c 100644 --- a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts @@ -1036,7 +1036,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { ); const providerOptions = input.providerOptions?.claudeCode; - const permissionMode = toPermissionMode(providerOptions?.permissionMode); + const permissionMode = + toPermissionMode(providerOptions?.permissionMode) ?? + (input.approvalPolicy === "never" ? "bypassPermissions" : undefined); const queryOptions: ClaudeQueryOptions = { ...(input.cwd ? { cwd: input.cwd } : {}), diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 60d2f0ba4d9752e8520f349000020b5854e36bb3..37bc20b5418be8bf127145241d2921506fdd2a7c 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -8,12 +8,12 @@ import { type MessageId, getDefaultModel, getModelOptions, + getReasoningOptions, type ProjectId, type ProjectEntry, type ProjectScript, PROVIDER_SEND_TURN_MAX_ATTACHMENTS, PROVIDER_SEND_TURN_MAX_IMAGE_BYTES, - REASONING_OPTIONS, type ReasoningEffort, type ResolvedKeybindingsConfig, type ProviderApprovalDecision, @@ -659,6 +659,8 @@ export default function ChatView({ threadId }: ChatViewProps) { ); const selectedModel = resolveModelSlugForProvider(selectedProvider, composerDraft.model ?? baseThreadModel); const selectedEffort = composerDraft.effort ?? DEFAULT_REASONING; + const reasoningOptions = getReasoningOptions(selectedProvider); + const supportsReasoningEffort = reasoningOptions.length > 0; const searchableModelOptions = useMemo( () => PROVIDER_OPTIONS.filter((option) => option.available).flatMap((option) => @@ -2168,7 +2170,7 @@ export default function ChatView({ threadId }: ChatViewProps) { }, model: selectedModel || undefined, provider: selectedProvider, - effort: selectedEffort || undefined, + ...(supportsReasoningEffort ? { effort: selectedEffort } : {}), assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", approvalPolicy, sandboxMode, @@ -2722,10 +2724,17 @@ export default function ChatView({ threadId }: ChatViewProps) { {/* Reasoning effort */} - - - {/* Divider */} - + {supportsReasoningEffort ? ( + <> + + {/* Divider */} + + + ) : null} {/* Runtime mode toggle */}