Skip to content

feat: add Claude Code adapter#179

Open
juliusmarminge wants to merge 31 commits intomainfrom
codething/648ca884-claude
Open

feat: add Claude Code adapter#179
juliusmarminge wants to merge 31 commits intomainfrom
codething/648ca884-claude

Conversation

@juliusmarminge
Copy link
Member

@juliusmarminge juliusmarminge commented Mar 6, 2026

Summary

This PR adds the Claude Code adapter on top of the core orchestration branch in #103.

It includes:

  • the Claude Code provider adapter and service layer
  • provider registry/server-layer wiring for Claude Code
  • Claude Code availability in the provider/session UI surface needed for this stack

Stack

Validation

  • bun lint
  • bun typecheck
  • cd apps/server && bun run test -- --run src/provider/Layers/ProviderAdapterRegistry.test.ts
  • cd apps/web && bun run test -- --run src/session-logic.test.ts

Note

High Risk
Adds a new claudeAgent provider with session lifecycle, resume, approvals/user-input bridging, and rollback behavior, plus changes to provider/session validation and restart rules; mistakes could break turn execution, approval gating, or checkpoint reverts across providers.

Overview
Adds first-class support for the claudeAgent provider end-to-end, including a new ClaudeAdapterLive backed by @anthropic-ai/claude-agent-sdk that streams canonical ProviderRuntimeEvents, supports interrupts, approvals/user-input requests, resume cursors, and thread rollback.

Updates orchestration/provider plumbing to be provider-aware: ProviderKind and model catalogs now include Claude models and options, ProviderCommandReactor enforces provider/model compatibility, restarts Claude sessions when Claude modelOptions change, and surfaces turn-start failures as thread activities instead of crashing.

Extends ingestion and checkpointing tests/behavior for Claude events (turn lifecycle, task progress summaries, checkpoint capture/revert), and adds new integration coverage for first-turn Claude selection, stopAll recovery via persisted resume state, approval response forwarding, and interrupt forwarding. Also refreshes docs/README to reflect Claude as supported.

Written by Cursor Bugbot for commit a75a9b4. This will update automatically on new commits. Configure here.

Note

Add Claude Code adapter with full provider support across orchestration, UI, and persistence

  • Introduces a new claudeAgent provider backed by @anthropic-ai/claude-agent-sdk, implemented in ClaudeAdapter.ts with session/turn handling, approvals, user input, and in-session model/permission updates.
  • Adds claudeAgent to contracts (ProviderKind, ProviderStartOptions, model/effort/alias registries) and the provider adapter registry, health checks, and session directory.
  • Extends the composer draft store to replace legacy codex-only fields (effort, codexFastMode) with a unified per-provider modelOptions map, with versioned migration for older persisted state.
  • Adds ClaudeTraitsPicker and updates CodexTraitsPicker to read/write from the draft store; the ProviderModelPicker gains ultrathinkActive visual state and provider-locked model listing.
  • ProviderCommandReactor enforces provider binding per thread and triggers session restarts when Claude modelOptions change across turns.
  • Health service now checks Claude CLI availability and authentication status in parallel with Codex.
  • Risk: persisted composer draft state schema is versioned and migrated; malformed or unversioned blobs are discarded rather than migrated.

Macroscope summarized aa66e08.

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 42d4266e-7e70-4ed4-bdaf-95695a427fe8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codething/648ca884-claude
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: SDK stream fiber is detached and unmanaged
    • Replaced Effect.runFork with Effect.forkChild to keep the fiber in the managed runtime, stored the fiber reference in session context, and added explicit Fiber.interrupt in stopSessionInternal before queue teardown to eliminate the race window.
  • ✅ Fixed: Identity function adds unnecessary indirection
    • Removed the no-op asCanonicalTurnId identity function and inlined the TurnId value directly at all 10 call sites.

Create PR

Or push these changes by commenting:

@cursor push 2efc9d6c0c
Preview (2efc9d6c0c)
diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
--- a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
+++ b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
@@ -34,7 +34,7 @@
   ThreadId,
   TurnId,
 } from "@t3tools/contracts";
-import { Cause, DateTime, Deferred, Effect, Layer, Queue, Random, Ref, Stream } from "effect";
+import { Cause, DateTime, Deferred, Effect, Fiber, Layer, Queue, Random, Ref, Stream } from "effect";
 
 import {
   ProviderAdapterProcessError,
@@ -106,6 +106,7 @@
   lastAssistantUuid: string | undefined;
   lastThreadStartedId: string | undefined;
   stopped: boolean;
+  streamFiber: Fiber.Fiber<void, never> | undefined;
 }
 
 interface ClaudeQueryRuntime extends AsyncIterable<SDKMessage> {
@@ -144,10 +145,6 @@
   return RuntimeItemId.makeUnsafe(value);
 }
 
-function asCanonicalTurnId(value: TurnId): TurnId {
-  return value;
-}
-
 function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId {
   return RuntimeRequestId.makeUnsafe(value);
 }
@@ -505,7 +502,7 @@
                 ...(typeof message.session_id === "string"
                   ? { providerThreadId: message.session_id }
                   : {}),
-                ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
+                ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
                 ...(itemId ? { itemId: ProviderItemId.makeUnsafe(itemId) } : {}),
                 payload: message,
               },
@@ -613,7 +610,7 @@
           provider: PROVIDER,
           createdAt: stamp.createdAt,
           threadId: context.session.threadId,
-          ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}),
+          ...(turnState ? { turnId: turnState.turnId } : {}),
           payload: {
             message,
             class: "provider_error",
@@ -640,7 +637,7 @@
           provider: PROVIDER,
           createdAt: stamp.createdAt,
           threadId: context.session.threadId,
-          ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}),
+          ...(turnState ? { turnId: turnState.turnId } : {}),
           payload: {
             message,
             ...(detail !== undefined ? { detail } : {}),
@@ -855,7 +852,7 @@
             provider: PROVIDER,
               createdAt: stamp.createdAt,
             threadId: context.session.threadId,
-            ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
+            ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
             itemId: asRuntimeItemId(tool.itemId),
             payload: {
               itemType: tool.itemType,
@@ -896,7 +893,7 @@
             provider: PROVIDER,
               createdAt: stamp.createdAt,
             threadId: context.session.threadId,
-            ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
+            ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
             itemId: asRuntimeItemId(tool.itemId),
             payload: {
               itemType: tool.itemType,
@@ -1006,7 +1003,7 @@
           provider: PROVIDER,
           createdAt: stamp.createdAt,
           threadId: context.session.threadId,
-          ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
+          ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
           providerRefs: {
             ...providerThreadRef(context),
             ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}),
@@ -1165,7 +1162,7 @@
           provider: PROVIDER,
           createdAt: stamp.createdAt,
           threadId: context.session.threadId,
-          ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
+          ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
           providerRefs: {
             ...providerThreadRef(context),
             ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}),
@@ -1295,6 +1292,11 @@
 
         context.stopped = true;
 
+        if (context.streamFiber) {
+          yield* Fiber.interrupt(context.streamFiber);
+          context.streamFiber = undefined;
+        }
+
         for (const [requestId, pending] of context.pendingApprovals) {
           yield* Deferred.succeed(pending.decision, "cancel");
           const stamp = yield* makeEventStamp();
@@ -1304,7 +1306,7 @@
             provider: PROVIDER,
               createdAt: stamp.createdAt,
             threadId: context.session.threadId,
-            ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
+            ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
             requestId: asRuntimeRequestId(requestId),
             payload: {
               requestType: pending.requestType,
@@ -1442,7 +1444,7 @@
                 provider: PROVIDER,
                       createdAt: requestedStamp.createdAt,
                 threadId: context.session.threadId,
-                ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
+                ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
                 requestId: asRuntimeRequestId(requestId),
                 payload: {
                   requestType,
@@ -1494,7 +1496,7 @@
                 provider: PROVIDER,
                       createdAt: resolvedStamp.createdAt,
                 threadId: context.session.threadId,
-                ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),
+                ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
                 requestId: asRuntimeRequestId(requestId),
                 payload: {
                   requestType,
@@ -1610,6 +1612,7 @@
           lastAssistantUuid: resumeState?.resumeSessionAt,
           lastThreadStartedId: undefined,
           stopped: false,
+          streamFiber: undefined,
         };
         yield* Ref.set(contextRef, context);
         sessions.set(threadId, context);
@@ -1658,7 +1661,7 @@
           providerRefs: {},
         });
 
-        Effect.runFork(runSdkStream(context));
+        context.streamFiber = yield* Effect.forkChild(runSdkStream(context));
 
         return {
           ...session,

@juliusmarminge juliusmarminge force-pushed the codething/648ca884-claude branch from 2b53034 to 1beeff2 Compare March 6, 2026 04:58
Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Native event logger missing threadId in event payload
    • Added threadId: context.session.threadId to the event object and passed context.session.threadId instead of null as the second argument to nativeEventLogger.write(), enabling per-thread log routing consistent with the Codex adapter.

Create PR

Or push these changes by commenting:

@cursor push 8489584240
Preview (8489584240)
diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
--- a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
+++ b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
@@ -502,6 +502,7 @@
                 provider: PROVIDER,
                 createdAt: observedAt,
                 method: sdkNativeMethod(message),
+                threadId: context.session.threadId,
                 ...(typeof message.session_id === "string"
                   ? { providerThreadId: message.session_id }
                   : {}),
@@ -510,7 +511,7 @@
                 payload: message,
               },
             },
-            null,
+            context.session.threadId,
           );
       });

@juliusmarminge juliusmarminge force-pushed the codething/648ca884-claude branch 4 times, most recently from 4afd04a to 3af67f5 Compare March 6, 2026 05:37
Comment on lines +29 to +31
customClaudeModels: Schema.Array(Schema.String).pipe(
Schema.withConstructorDefault(() => Option.some([])),
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low src/appSettings.ts:29

Adding customClaudeModels as a required field causes Schema.decodeSync to throw when decoding existing localStorage data that lacks this key. The caught error triggers fallback to DEFAULT_APP_SETTINGS, permanently discarding the user's saved configuration. Mark the field optional with a decode-time default instead of using withConstructorDefault.

-  customClaudeModels: Schema.Array(Schema.String).pipe(
-    Schema.withConstructorDefault(() => Option.some([])),
-  ),
Also found in 1 other location(s)

apps/web/src/routes/_chat.settings.tsx:65

The getCustomModelsForProvider function directly returns settings.customClaudeModels. Since customClaudeModels is a new property added to the schema in this PR, the persisted settings object in local storage for existing users will not contain this key. This causes the function to return undefined. Consuming code (e.g., SettingsRouteView at lines 164 and 398) assumes an array and will crash with a TypeError when accessing .length or .includes() on undefined. The accessor should default to an empty array (e.g., settings.customClaudeModels ?? []) to handle the schema migration for existing data.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/appSettings.ts around lines 29-31:

Adding `customClaudeModels` as a required field causes `Schema.decodeSync` to throw when decoding existing `localStorage` data that lacks this key. The caught error triggers fallback to `DEFAULT_APP_SETTINGS`, permanently discarding the user's saved configuration. Mark the field optional with a decode-time default instead of using `withConstructorDefault`.

Evidence trail:
apps/web/src/appSettings.ts lines 29-31 (customClaudeModels definition with withConstructorDefault), lines 134-143 (parsePersistedSettings using decodeSync with catch fallback to DEFAULT_APP_SETTINGS). Git diff (MERGE_BASE..REVIEWED_COMMIT) shows customClaudeModels being added. Effect-TS issue #1997 (https://github.com/Effect-TS/effect/issues/1997) confirms withConstructorDefault provides defaults at construction time only, not during decode: "I often find myself needing to provide default values at construction, but not at parsing."

Also found in 1 other location(s):
- apps/web/src/routes/_chat.settings.tsx:65 -- The `getCustomModelsForProvider` function directly returns `settings.customClaudeModels`. Since `customClaudeModels` is a new property added to the schema in this PR, the persisted `settings` object in local storage for existing users will not contain this key. This causes the function to return `undefined`. Consuming code (e.g., `SettingsRouteView` at lines 164 and 398) assumes an array and will crash with a `TypeError` when accessing `.length` or `.includes()` on `undefined`. The accessor should default to an empty array (e.g., `settings.customClaudeModels ?? []`) to handle the schema migration for existing data.

Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Redundant duplicate threadId assignment in session construction
    • Removed the redundant ...(threadId ? { threadId } : {}) spread at line 1587 since threadId is already set directly at line 1581 and is always defined.

Create PR

Or push these changes by commenting:

@cursor push 1970102289
Preview (1970102289)
diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
--- a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
+++ b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts
@@ -1584,7 +1584,6 @@
           runtimeMode: input.runtimeMode,
           ...(input.cwd ? { cwd: input.cwd } : {}),
           ...(input.model ? { model: input.model } : {}),
-          ...(threadId ? { threadId } : {}),
           resumeCursor: {
             ...(threadId ? { threadId } : {}),
             ...(resumeState?.resume ? { resume: resumeState.resume } : {}),

Base automatically changed from codething/648ca884 to main March 6, 2026 07:00
Comment on lines +807 to +820
async respondToUserInput(
threadId: ThreadId,
requestId: ApprovalRequestId,
answers: ProviderUserInputAnswers,
): Promise<void> {
const context = this.requireSession(threadId);
const pendingRequest = context.pendingUserInputs.get(requestId);
if (!pendingRequest) {
throw new Error(`Unknown pending user input request: ${requestId}`);
}

context.pendingUserInputs.delete(requestId);
const codexAnswers = toCodexUserInputAnswers(answers);
this.writeMessage(context, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/codexAppServerManager.ts:807

In respondToUserInput, context.pendingUserInputs.delete(requestId) is called before validating answers via toCodexUserInputAnswers. If conversion throws, the request record is permanently lost but no response reaches the provider, leaving the session corrupted and blocking retries. Move the deletion after successful validation and conversion.

   async respondToUserInput(
     threadId: ThreadId,
     requestId: ApprovalRequestId,
     answers: ProviderUserInputAnswers,
   ): Promise<void> {
     const context = this.requireSession(threadId);
     const pendingRequest = context.pendingUserInputs.get(requestId);
     if (!pendingRequest) {
       throw new Error(`Unknown pending user input request: ${requestId}`);
     }
 
-    context.pendingUserInputs.delete(requestId);
     const codexAnswers = toCodexUserInputAnswers(answers);
+    context.pendingUserInputs.delete(requestId);
     this.writeMessage(context, {
       id: pendingRequest.jsonRpcId,
       result: {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/codexAppServerManager.ts around lines 807-820:

In `respondToUserInput`, `context.pendingUserInputs.delete(requestId)` is called before validating `answers` via `toCodexUserInputAnswers`. If conversion throws, the request record is permanently lost but no response reaches the provider, leaving the session corrupted and blocking retries. Move the deletion after successful validation and conversion.

Evidence trail:
apps/server/src/codexAppServerManager.ts lines 807-825 (REVIEWED_COMMIT): `respondToUserInput` method showing delete at line 818, conversion at line 819
apps/server/src/codexAppServerManager.ts lines 358-381 (REVIEWED_COMMIT): `toCodexUserInputAnswer` function that throws Error at line 370, and `toCodexUserInputAnswers` that calls it
Line 818: `context.pendingUserInputs.delete(requestId);`
Line 819: `const codexAnswers = toCodexUserInputAnswers(answers);`
Line 370: `throw new Error("User input answers must be strings or arrays of strings.");`

this.runPromise = services ? Effect.runPromiseWith(services) : Effect.runPromise;
}

async startSession(input: CodexAppServerStartSessionInput): Promise<ProviderSession> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/codexAppServerManager.ts:428

startSession overwrites this.sessions.set(threadId, context) without checking if a session already exists for that threadId. This orphans the previous process (which keeps running) and leaves its exit handler active. When the orphaned process later exits, that handler calls this.sessions.delete(threadId), which removes the new session from the map. Subsequent calls like sendTurn then fail with "Unknown session" because the entry was deleted by the stale handler.

Also found in 1 other location(s)

apps/server/src/provider/Layers/CodexAdapter.ts:1262

The nativeEventLogger created when options.nativeEventLogPath is provided is never closed. While manager is wrapped in Effect.acquireRelease for cleanup, nativeEventLogger is instantiated separately and its close() method is never called. This causes file handles opened by the logger to leak every time the CodexAdapter layer is released (e.g., during configuration reloads or tests).

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/codexAppServerManager.ts around line 428:

`startSession` overwrites `this.sessions.set(threadId, context)` without checking if a session already exists for that `threadId`. This orphans the previous process (which keeps running) and leaves its `exit` handler active. When the orphaned process later exits, that handler calls `this.sessions.delete(threadId)`, which removes the *new* session from the map. Subsequent calls like `sendTurn` then fail with "Unknown session" because the entry was deleted by the stale handler.

Evidence trail:
apps/server/src/codexAppServerManager.ts: lines 428-471 show `startSession` calls `this.sessions.set(threadId, context)` without checking for existing session; line 943 shows exit handler calls `this.sessions.delete(context.session.threadId)` where `context` is captured in closure; lines 890-893 show `requireSession` throws 'Unknown session' when entry not in map; lines 606-607 show `sendTurn` uses `requireSession`.

Also found in 1 other location(s):
- apps/server/src/provider/Layers/CodexAdapter.ts:1262 -- The `nativeEventLogger` created when `options.nativeEventLogPath` is provided is never closed. While `manager` is wrapped in `Effect.acquireRelease` for cleanup, `nativeEventLogger` is instantiated separately and its `close()` method is never called. This causes file handles opened by the logger to leak every time the `CodexAdapter` layer is released (e.g., during configuration reloads or tests).

Comment on lines +329 to +341
export const OpenCodeIcon: Icon = (props) => (
<svg {...props} viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#opencode__clip0_1311_94969)">
<path d="M24 32H8V16H24V32Z" fill="#BCBBBB" />
<path d="M24 8H8V32H24V8ZM32 40H0V0H32V40Z" fill="#211E1E" />
</g>
<defs>
<clipPath id="opencode__clip0_1311_94969">
<rect width="32" height="40" fill="white" />
</clipPath>
</defs>
</svg>
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low components/Icons.tsx:329

The OpenCodeIcon component uses hardcoded fill attributes (#211E1E and #BCBBBB) on its SVG paths. In dark mode, the #211E1E dark grey fill renders the icon nearly invisible due to low contrast against dark backgrounds. Unlike GitHubIcon and OpenAI, which use currentColor to inherit the surrounding text color, this icon fails to adapt to the theme.

-  <svg {...props} viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <svg {...props} viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
     <g clipPath="url(#opencode__clip0_1311_94969)">
-      <path d="M24 32H8V16H24V32Z" fill="#BCBBBB" />
-      <path d="M24 8H8V32H24V8ZM32 40H0V0H32V40Z" fill="#211E1E" />
+      <path d="M24 32H8V16H24V32Z" fill="currentColor" />
+      <path d="M24 8H8V32H24V8ZM32 40H0V0H32V40Z" fill="currentColor" />
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/components/Icons.tsx around lines 329-341:

The `OpenCodeIcon` component uses hardcoded `fill` attributes (`#211E1E` and `#BCBBBB`) on its SVG paths. In dark mode, the `#211E1E` dark grey fill renders the icon nearly invisible due to low contrast against dark backgrounds. Unlike `GitHubIcon` and `OpenAI`, which use `currentColor` to inherit the surrounding text color, this icon fails to adapt to the theme.

Evidence trail:
apps/web/src/components/Icons.tsx lines 329-342 (OpenCodeIcon with hardcoded fills #211E1E and #BCBBBB), lines 5-15 (GitHubIcon with fill="currentColor" at line 12), lines 146-150 (OpenAI with fill="currentColor" at line 147). Verified at commit REVIEWED_COMMIT.

@t3dotgg
Copy link
Member

t3dotgg commented Mar 6, 2026

@bcherny @ThariqS mind giving this an approval so we know we can ship it safely? Would hate for our users to get banned 😭

ben-vargas added a commit to ben-vargas/ai-t3code that referenced this pull request Mar 7, 2026
@dl-alexandre
Copy link

I prepared a CI fix PR targeting this branch: #243\n\nIt updates stale ClaudeCodeAdapter test expectations for thread identity/providerThreadId behavior and aligns with current adapter semantics.

Ascinocco added a commit to Ascinocco/t3code that referenced this pull request Mar 7, 2026
Fix detached SDK stream fiber by replacing Effect.runFork with
Effect.forkChild and adding explicit Fiber.interrupt on session stop.
Add missing threadId to native event logger for per-thread log routing.
Remove no-op asCanonicalTurnId identity function. Remove redundant
threadId spread in session construction. Fix stale test expectations
for thread identity behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gabrielMalonso added a commit to gabrielMalonso/t3code that referenced this pull request Mar 7, 2026
…apters

Merge the Claude Code adapter (PR pingdotgg#179) into main, resolving 45 conflicts
caused by the deliberate split of provider stacks on March 5.

Key additions:
- ClaudeCodeAdapter with full session, turn, and resume lifecycle
- Cursor provider support (model catalog, UI, routing)
- ProviderKind expanded from "codex" to "codex" | "claudeCode" | "cursor"
- Provider model catalogs, aliases, and slug resolution across all providers
- UI support for Claude Code and Cursor in ChatView, settings, and composer

Conflict resolution strategy:
- Kept HEAD's refactored patterns (scoped finalizers, telemetry, serviceTier)
- Added PR's new provider routing, adapters, and UI components
- Fixed duplicate declarations, missing props, and type mismatches

Typecheck: 7/7 packages pass
Lint: 0 warnings, 0 errors
Tests: 419/422 pass (3 pre-existing failures in ClaudeCodeAdapter.test.ts)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gabrielMalonso added a commit to gabrielMalonso/t3code that referenced this pull request Mar 7, 2026
Remove filtro que escondia o Claude Code do seletor de providers,
tornando-o selecionável após o merge do adapter (PR pingdotgg#179).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
hameltomor added a commit to hameltomor/t3code that referenced this pull request Mar 7, 2026
Add first-class Claude Code support alongside existing Codex provider:

- Add ClaudeCodeAdapter (1857 lines) backed by @anthropic-ai/claude-agent-sdk
- Extend ProviderKind to accept "codex" | "claudeCode"
- Add Claude model catalog (Opus 4.6, Sonnet 4.6, Haiku 4.5)
- Register ClaudeCodeAdapter in provider registry and server layers
- Add Claude SDK event sources to provider runtime events
- Enable Claude Code in UI provider picker
- Add ClaudeCodeProviderStartOptions to contracts
- Update all tests for multi-provider support

Based on upstream PR pingdotgg#179 by juliusmarminge, surgically applied to current main.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
souravrs999 added a commit to souravrs999/t3code that referenced this pull request Mar 8, 2026
Merges codething/648ca884-claude branch which adds full Claude Code
provider adapter, orchestration enhancements, and UI surface.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@maria-rcks maria-rcks closed this Mar 9, 2026
@maria-rcks maria-rcks reopened this Mar 9, 2026
@github-actions github-actions bot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Mar 9, 2026
Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Turn start error handler catches interrupts as failures
    • Added a Cause.hasInterruptsOnly check in the inner catchCause handler to re-propagate interrupt causes instead of converting them into spurious provider.turn.start.failed activities.

Create PR

Or push these changes by commenting:

@cursor push ae48a85907
Preview (ae48a85907)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
@@ -498,16 +498,19 @@
       interactionMode: event.payload.interactionMode,
       createdAt: event.payload.createdAt,
     }).pipe(
-      Effect.catchCause((cause) =>
-        appendProviderFailureActivity({
+      Effect.catchCause((cause) => {
+        if (Cause.hasInterruptsOnly(cause)) {
+          return Effect.failCause(cause);
+        }
+        return appendProviderFailureActivity({
           threadId: event.payload.threadId,
           kind: "provider.turn.start.failed",
           summary: "Provider turn start failed",
           detail: Cause.pretty(cause),
           turnId: null,
           createdAt: event.payload.createdAt,
-        }),
-      ),
+        });
+      }),
     );
   });

createdAt: event.payload.createdAt,
}),
),
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turn start error handler catches interrupts as failures

Low Severity

The new Effect.catchCause wrapper around sendTurnForThread catches all causes including interrupts. When a fiber is interrupted (e.g., during shutdown), this creates a spurious provider.turn.start.failed activity instead of allowing the interrupt to propagate. The outer processDomainEventSafely handler already filters interrupts via Cause.hasInterruptsOnly, but this inner handler runs first and swallows them.

Fix in Cursor Fix in Web

- derive `defaultModel` from `DEFAULT_MODEL_BY_PROVIDER` using harness provider
- replace hardcoded `gpt-5-codex` in seeded project and thread setup
Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Redundant variable always equals existing value
    • Removed the redundant preferredProvider variable and replaced its two usages with threadProvider, which is always equal.

Create PR

Or push these changes by commenting:

@cursor push 7b50561688
Preview (7b50561688)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
@@ -229,7 +229,6 @@
         detail: `Model '${options.model}' does not belong to provider '${threadProvider}' for thread '${threadId}'.`,
       });
     }
-    const preferredProvider: ProviderKind = currentProvider ?? threadProvider;
     const desiredModel = options?.model ?? thread.model;
     const effectiveCwd = resolveThreadWorkspaceCwd({
       thread,
@@ -247,8 +246,8 @@
     }) =>
       providerService.startSession(threadId, {
         threadId,
-        ...((input?.provider ?? preferredProvider)
-          ? { provider: input?.provider ?? preferredProvider }
+        ...((input?.provider ?? threadProvider)
+          ? { provider: input?.provider ?? threadProvider }
           : {}),
         ...(effectiveCwd ? { cwd: effectiveCwd } : {}),
         ...(desiredModel ? { model: desiredModel } : {}),

…1146)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@juliusmarminge
Copy link
Member Author

@JustYannicc should the ultrathink UI trigger here?

CleanShot 2026-03-17 at 13 26 13@2x

Comment on lines +2502 to +2511
yield* Effect.addFinalizer(() =>
Effect.forEach(
sessions,
([, context]) =>
stopSessionInternal(context, {
emitExitEvent: false,
}),
{ discard: true },
).pipe(Effect.tap(() => Queue.shutdown(runtimeEventQueue))),
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/ClaudeAdapter.ts:2502

When nativeEventLogPath is provided, makeClaudeAdapter creates an EventNdjsonLogger internally at line 644, but the finalizer (lines 2502-2511) never calls nativeEventLogger.close(). This leaves file handles held by the logger's thread writers open until process exit. Consider calling nativeEventLogger.close() in the finalizer when the logger was created internally.

    yield* Effect.addFinalizer(() =>
      Effect.forEach(
        sessions,
        ([, context]) =>
          stopSessionInternal(context, {
            emitExitEvent: false,
          }),
        { discard: true },
-      ).pipe(Effect.tap(() => Queue.shutdown(runtimeEventQueue))),
+      ).pipe(
+        Effect.tap(() => Queue.shutdown(runtimeEventQueue)),
+        Effect.tap(() =>
+          nativeEventLogger ? nativeEventLogger.close() : Effect.void
+        )
+      ),
    );
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/ClaudeAdapter.ts around lines 2502-2511:

When `nativeEventLogPath` is provided, `makeClaudeAdapter` creates an `EventNdjsonLogger` internally at line 644, but the finalizer (lines 2502-2511) never calls `nativeEventLogger.close()`. This leaves file handles held by the logger's thread writers open until process exit. Consider calling `nativeEventLogger.close()` in the finalizer when the logger was created internally.

Evidence trail:
- apps/server/src/provider/Layers/ClaudeAdapter.ts lines 641-646 (REVIEWED_COMMIT): Shows `nativeEventLogger` is created internally via `makeEventNdjsonLogger` when `nativeEventLogPath` is provided
- apps/server/src/provider/Layers/ClaudeAdapter.ts lines 2502-2511 (REVIEWED_COMMIT): Finalizer only stops sessions and shuts down queue, no `nativeEventLogger.close()` call
- apps/server/src/provider/Layers/EventNdjsonLogger.ts lines 25-29, 188-195 (REVIEWED_COMMIT): Shows `EventNdjsonLogger` interface has a `close()` method that closes all thread writers
- git_grep for `nativeEventLogger.close` in ClaudeAdapter.ts: No results, confirming `close()` is never called

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low

The if (!isUnknownPendingApprovalRequestError(cause)) return; statement at line 596 is dead code — there are no subsequent statements in the generator, so the return has no effect and the generator completes with void either way. This appears to be incomplete logic where additional handling was intended for the unknown pending approval request case.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/orchestration/Layers/ProviderCommandReactor.ts around line 595:

The `if (!isUnknownPendingApprovalRequestError(cause)) return;` statement at line 596 is dead code — there are no subsequent statements in the generator, so the return has no effect and the generator completes with `void` either way. This appears to be incomplete logic where additional handling was intended for the unknown pending approval request case.

Evidence trail:
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts lines 585-598 at REVIEWED_COMMIT - The generator function contains `if (!isUnknownPendingApprovalRequestError(cause)) return;` at line 596, followed immediately by the closing of the generator (`}),`). No statements follow the if statement, making the return statement have no observable effect.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium

yield* upsertSessionBinding(resumed, input.binding.threadId);

In recoverSessionForThread, upsertSessionBinding is called at line 247 without passing persistedModelOptions and persistedProviderOptions, even though these values were extracted and used to start the session. After recovery, the runtime payload drops these options, so subsequent operations that read the persisted binding won't see the original model/provider configuration.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/ProviderService.ts around line 247:

In `recoverSessionForThread`, `upsertSessionBinding` is called at line 247 without passing `persistedModelOptions` and `persistedProviderOptions`, even though these values were extracted and used to start the session. After recovery, the runtime payload drops these options, so subsequent operations that read the persisted binding won't see the original model/provider configuration.

Evidence trail:
apps/server/src/provider/Layers/ProviderService.ts lines 229-230 (extraction of persistedModelOptions and persistedProviderOptions), lines 234-235 (passing them to startSession), line 247 (upsertSessionBinding called without extra parameter), lines 162-177 (upsertSessionBinding signature showing optional extra parameter), lines 88-99 (toRuntimePayloadFromSession showing options only included if extra is provided), lines 308-311 (startSession correctly passing options to upsertSessionBinding)

Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 10 total unresolved issues (including 9 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: New Claude unit tests broken by their own provider validation
    • Each Claude test now creates a dedicated thread with model "claude-sonnet-4-6" so that inferProviderForModel returns "claudeAgent" and the provider validation passes, allowing sessions to start correctly.

Create PR

Or push these changes by commenting:

@cursor push cda0e97dae
Preview (cda0e97dae)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
@@ -357,9 +357,25 @@
 
     await Effect.runPromise(
       harness.engine.dispatch({
+        type: "thread.create",
+        commandId: CommandId.makeUnsafe("cmd-thread-create-claude"),
+        threadId: ThreadId.makeUnsafe("thread-claude"),
+        projectId: asProjectId("project-1"),
+        title: "Claude Thread",
+        model: "claude-sonnet-4-6",
+        interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
+        runtimeMode: "approval-required",
+        branch: null,
+        worktreePath: null,
+        createdAt: now,
+      }),
+    );
+
+    await Effect.runPromise(
+      harness.engine.dispatch({
         type: "thread.turn.start",
         commandId: CommandId.makeUnsafe("cmd-turn-start-claude-effort"),
-        threadId: ThreadId.makeUnsafe("thread-1"),
+        threadId: ThreadId.makeUnsafe("thread-claude"),
         message: {
           messageId: asMessageId("user-message-claude-effort"),
           role: "user",
@@ -391,7 +407,7 @@
       },
     });
     expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({
-      threadId: ThreadId.makeUnsafe("thread-1"),
+      threadId: ThreadId.makeUnsafe("thread-claude"),
       model: "claude-sonnet-4-6",
       modelOptions: {
         claudeAgent: {
@@ -587,9 +603,25 @@
 
     await Effect.runPromise(
       harness.engine.dispatch({
+        type: "thread.create",
+        commandId: CommandId.makeUnsafe("cmd-thread-create-claude-restart"),
+        threadId: ThreadId.makeUnsafe("thread-claude-restart"),
+        projectId: asProjectId("project-1"),
+        title: "Claude Restart Thread",
+        model: "claude-sonnet-4-6",
+        interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
+        runtimeMode: "approval-required",
+        branch: null,
+        worktreePath: null,
+        createdAt: now,
+      }),
+    );
+
+    await Effect.runPromise(
+      harness.engine.dispatch({
         type: "thread.turn.start",
         commandId: CommandId.makeUnsafe("cmd-turn-start-claude-effort-1"),
-        threadId: ThreadId.makeUnsafe("thread-1"),
+        threadId: ThreadId.makeUnsafe("thread-claude-restart"),
         message: {
           messageId: asMessageId("user-message-claude-effort-1"),
           role: "user",
@@ -616,7 +648,7 @@
       harness.engine.dispatch({
         type: "thread.turn.start",
         commandId: CommandId.makeUnsafe("cmd-turn-start-claude-effort-2"),
-        threadId: ThreadId.makeUnsafe("thread-1"),
+        threadId: ThreadId.makeUnsafe("thread-claude-restart"),
         message: {
           messageId: asMessageId("user-message-claude-effort-2"),
           role: "user",

@JustYannicc
Copy link

No you have to select Ultra think in the thinking level but can add that

@juliusmarminge
Copy link
Member Author

Also Haiku doesn't support ultrathink but it's selectable in the UI

CleanShot 2026-03-17 at 14 07 48@2x CleanShot 2026-03-17 at 14 07 42@2x

- keep Ultrathink frame styling without extra wrapper padding toggles
- drop the inline Ultrathink badge row in ChatView
- clean up stray whitespace in ProviderService test
Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 11 total unresolved issues (including 10 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: JSON.stringify comparison triggers spurious session restarts
    • Replaced JSON.stringify equality with a recursive structural deep-equal that compares objects by their defined keys and values regardless of property insertion order, preventing false negatives that would trigger unnecessary session restarts.

Create PR

Or push these changes by commenting:

@cursor push a325efe38a
Preview (a325efe38a)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
@@ -75,10 +75,23 @@
 const WORKTREE_BRANCH_PREFIX = "t3code";
 const TEMP_WORKTREE_BRANCH_PATTERN = new RegExp(`^${WORKTREE_BRANCH_PREFIX}\\/[0-9a-f]{8}$`);
 
+function structuralEqual(a: unknown, b: unknown): boolean {
+  if (a === b) return true;
+  if (a == null || b == null) return a === b;
+  if (typeof a !== typeof b) return false;
+  if (typeof a !== "object") return false;
+  const objA = a as Record<string, unknown>;
+  const objB = b as Record<string, unknown>;
+  const keysA = Object.keys(objA).filter((k) => objA[k] !== undefined);
+  const keysB = Object.keys(objB).filter((k) => objB[k] !== undefined);
+  if (keysA.length !== keysB.length) return false;
+  return keysA.every((key) => objB[key] !== undefined && structuralEqual(objA[key], objB[key]));
+}
+
 const sameModelOptions = (
   left: ProviderModelOptions | undefined,
   right: ProviderModelOptions | undefined,
-): boolean => JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
+): boolean => structuralEqual(left ?? null, right ?? null);
 
 function isUnknownPendingApprovalRequestError(cause: Cause.Cause<ProviderServiceError>): boolean {
   const error = Cause.squash(cause);

const sameModelOptions = (
left: ProviderModelOptions | undefined,
right: ProviderModelOptions | undefined,
): boolean => JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON.stringify comparison triggers spurious session restarts

Medium Severity

sameModelOptions uses JSON.stringify for equality, which is sensitive to property insertion order. Two ProviderModelOptions objects that are semantically identical but constructed with keys in different orders (e.g., from separate code paths or deserialized from different sources) will serialize to different strings, causing shouldRestartForModelOptionsChange to evaluate true and trigger an unnecessary Claude session restart — discarding the resume cursor and losing all conversation context in the process.

Fix in Cursor Fix in Web

- support `claudeAgent.fastMode` in shared/contracts, provider dispatch, and Claude adapter SDK settings
- gate fast mode to supported Claude models and restart/forward options in orchestration
- unify draft/UI fast mode state (not Codex-only) and add browser/server tests plus SDK probe
Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 11 total unresolved issues (including 9 from previous reviews).

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Resume cursor dropped unnecessarily on effort change
    • Removed shouldRestartForModelOptionsChange from the condition that drops resumeCursor, so effort/model-option changes now preserve conversation context like runtimeModeChanged does.
  • ✅ Fixed: Redundant tool classification conditions after inclusive check
    • Simplified the second if-block to only check normalized === "task", removing the three conditions already covered by the earlier normalized.includes("agent") check.

Create PR

Or push these changes by commenting:

@cursor push 63ff52e33a
Preview (63ff52e33a)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
@@ -311,7 +311,7 @@
       }
 
       const resumeCursor =
-        providerChanged || shouldRestartForModelChange || shouldRestartForModelOptionsChange
+        providerChanged || shouldRestartForModelChange
           ? undefined
           : (activeSession?.resumeCursor ?? undefined);
       yield* Effect.logInfo("provider command reactor restarting provider session", {

diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts
--- a/apps/server/src/provider/Layers/ClaudeAdapter.ts
+++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts
@@ -238,12 +238,7 @@
   if (normalized.includes("agent")) {
     return "collab_agent_tool_call";
   }
-  if (
-    normalized === "task" ||
-    normalized === "agent" ||
-    normalized.includes("subagent") ||
-    normalized.includes("sub-agent")
-  ) {
+  if (normalized === "task") {
     return "collab_agent_tool_call";
   }
   if (


const resumeCursor =
providerChanged || shouldRestartForModelChange
providerChanged || shouldRestartForModelChange || shouldRestartForModelOptionsChange
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resume cursor dropped unnecessarily on effort change

Medium Severity

When shouldRestartForModelOptionsChange is true (e.g., Claude effort changes from "medium" to "max"), the resumeCursor is set to undefined, causing the restarted session to lose all conversation context. Compare this with runtimeModeChanged, which preserves the resume cursor so conversation history survives the restart. The Claude SDK's resume parameter works independently of effort settings, so there's no technical reason to discard it. Users changing their thinking level will unexpectedly lose their entire conversation.

Additional Locations (1)
Fix in Cursor Fix in Web

normalized.includes("sub-agent")
) {
return "collab_agent_tool_call";
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant tool classification conditions after inclusive check

Low Severity

In classifyToolItemType, the first if block matches any tool name containing "agent" (normalized.includes("agent")). The second if block then redundantly re-checks normalized === "agent", normalized.includes("subagent"), and normalized.includes("sub-agent") — all already covered by the first check. Only normalized === "task" in the second block adds new logic. The redundancy obscures what the second block actually contributes.

Fix in Cursor Fix in Web

});
return;
}
const tool = context.inFlightTools.get(index);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low Layers/ClaudeAdapter.ts:1365

In the content_block_stop handler (line 1354), when the stopped block corresponds to a tool (not an assistant text block), the tool is fetched from context.inFlightTools at line 1365 but execution silently falls through without emitting any event. This means the final accumulated tool input from input_json_delta streaming is never published as an item.updated event when the content block finishes — the UI will show stale/partial tool input until the tool result message arrives later. The input_json_delta handler only emits updates when the fingerprint changes, so any final delta that didn't alter the fingerprint is lost. Add an item.updated emission (following the same pattern as the input_json_delta handler at lines 1256–1281) after line 1368 to publish the tool's final input state.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/ClaudeAdapter.ts around line 1365:

In the `content_block_stop` handler (line 1354), when the stopped block corresponds to a tool (not an assistant text block), the tool is fetched from `context.inFlightTools` at line 1365 but execution silently falls through without emitting any event. This means the final accumulated tool input from `input_json_delta` streaming is never published as an `item.updated` event when the content block finishes — the UI will show stale/partial tool input until the tool result message arrives later. The `input_json_delta` handler only emits updates when the fingerprint changes, so any final delta that didn't alter the fingerprint is lost. Add an `item.updated` emission (following the same pattern as the `input_json_delta` handler at lines 1256–1281) after line 1368 to publish the tool's final input state.

Evidence trail:
apps/server/src/provider/Layers/ClaudeAdapter.ts lines 1354-1369: `content_block_stop` handler shows the tool is fetched at line 1365 (`const tool = context.inFlightTools.get(index);`), then lines 1366-1368 only check `if (!tool) { return; }` with no code to handle when tool exists - the handler ends at line 1369.

apps/server/src/provider/Layers/ClaudeAdapter.ts lines 1242-1249: `input_json_delta` handler shows the fingerprint check that prevents emission when fingerprint is unchanged: `if (!parsedInput || !nextFingerprint || tool.lastEmittedInputFingerprint === nextFingerprint) { return; }`

apps/server/src/provider/Layers/ClaudeAdapter.ts lines 1256-1281: Shows the `item.updated` event emission pattern in `input_json_delta` handler that is missing from the `content_block_stop` tool handling.

@akarabach
Copy link

@juliusmarminge thanks for all the work you’ve put into this! Do you have an estimate for when it might be ready to land?

- Add dedicated Claude traits picker with model-aware effort, thinking, and fast mode controls
- Treat Claude Ultrathink as a prompt keyword instead of session effort
- Normalize provider model options in composer flow and adapter, with tests for unsupported effort/thinking cases
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium

setOptimisticUserMessages((existing) => [

When sending only images, the optimistic user message displays the internal IMAGE_ONLY_BOOTSTRAP_PROMPT text ("[User attached one or more images...]") in the chat bubble instead of showing only the attached images. This happens because onSend sets text: outgoingMessageText on line 2344, where outgoingMessageText falls back to the bootstrap prompt when the user provided no text. Previously the optimistic message used an empty text field for image-only sends, which correctly hid the internal instruction from users. Consider using an empty string for the optimistic message's text field when the original trimmed prompt was empty, keeping only the attachments visible.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/components/ChatView.tsx around line 2339:

When sending only images, the optimistic user message displays the internal `IMAGE_ONLY_BOOTSTRAP_PROMPT` text (`"[User attached one or more images...]"`) in the chat bubble instead of showing only the attached images. This happens because `onSend` sets `text: outgoingMessageText` on line 2344, where `outgoingMessageText` falls back to the bootstrap prompt when the user provided no text. Previously the optimistic message used an empty `text` field for image-only sends, which correctly hid the internal instruction from users. Consider using an empty string for the optimistic message's `text` field when the original `trimmed` prompt was empty, keeping only the attachments visible.

Evidence trail:
apps/web/src/components/ChatView.tsx lines 172-173: IMAGE_ONLY_BOOTSTRAP_PROMPT definition; lines 2319-2320: outgoingMessageText uses `trimmed || IMAGE_ONLY_BOOTSTRAP_PROMPT` fallback; line 2344: optimistic message sets `text: outgoingMessageText`

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low

function formatOutgoingPrompt(params: {

formatOutgoingPrompt checks params.effort === "ultrathink" to apply the prompt prefix, but selectedPromptEffort is computed from selectedClaudeBaseEffort which explicitly excludes "ultrathink". This branch is unreachable — the ultrathink prompt prefix is never applied during send, only when ClaudeTraitsMenuContent directly edits the prompt. Consider removing the dead branch or ensuring the prefix is applied consistently when ultrathink is active.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/components/ChatView.tsx around line 181:

`formatOutgoingPrompt` checks `params.effort === "ultrathink"` to apply the prompt prefix, but `selectedPromptEffort` is computed from `selectedClaudeBaseEffort` which explicitly excludes `"ultrathink"`. This branch is unreachable — the ultrathink prompt prefix is never applied during send, only when `ClaudeTraitsMenuContent` directly edits the prompt. Consider removing the dead branch or ensuring the prefix is applied consistently when ultrathink is active.

Evidence trail:
apps/web/src/components/ChatView.tsx:181-189 (formatOutgoingPrompt checks params.effort === "ultrathink"), apps/web/src/components/ChatView.tsx:570 (selectedPromptEffort = selectedCodexEffort ?? selectedClaudeBaseEffort), apps/web/src/components/ChatView.tsx:539-543 (selectedCodexEffort is null when provider is claudeAgent), apps/web/src/components/ChatView.tsx:548-565 (selectedClaudeBaseEffort explicitly excludes "ultrathink" at line 557: draftEffort !== "ultrathink"), apps/web/src/components/ChatView.tsx:2317-2320,2723-2726,2839-2842 (all calls pass selectedPromptEffort as effort)

@JustYannicc
Copy link

@akarabach you can just clone this branch, and build the application from here. it works. i have been using it.

@JustYannicc
Copy link

@juliusmarminge when actually using it as a DMG i noticed that there is a small bug that it doesnt use the system claude binaries but instead the bundles sdks binaries. I filled #1189 into this branch.

sohamnandi77 added a commit to sohamnandi77/t3code that referenced this pull request Mar 18, 2026
- Merge upstream Claude traits/model/UI changes from pingdotgg#179.
- Add ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN overrides and wire them through WS, server, Codex spawn env, and Claude env.
- Skip claude auth probe when external Anthropic token is configured.

Made-with: Cursor
dafzthomas added a commit to dafzthomas/t3code that referenced this pull request Mar 18, 2026
…esolution

Merge origin/codething/648ca884-claude into main, resolving conflicts in:
- apps/web/src/appSettings.ts (keep both textGenerationModel and customClaudeModels)
- apps/web/src/components/ChatView.tsx (take PR's restructured compositor layout)
- apps/web/src/composerDraftStore.ts (take PR's modelOptions migration, restore prompt/terminalContexts processing)
- packages/contracts/src/model.ts (keep both git text generation model and backward compat exports)

Additional fixes on top of the merge:
- Add terminalContexts to browser test mock drafts (ClaudeTraitsPicker, CodexTraitsPicker, CompactComposerControlsMenu)
- Restore ensureInlineTerminalContextPlaceholders call in draft deserialization
- Add terminalContexts and onRemoveTerminalContext props to ComposerPromptEditor
- Use DEFAULT_MODEL_BY_PROVIDER[selectedProvider] instead of hardcoded .codex
- Persist provider/model before clearComposerDraftContent to prevent reset

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dafzthomas added a commit to dafzthomas/t3code that referenced this pull request Mar 18, 2026
Merges latest commits from pingdotgg#179 including:
- Storage refactor (composerDraftStore extracted resolveModelOptions)
- setState pattern refactored in browser test fixtures
- Upstream main merged in (terminal context, sidebar fixes, git text gen)
- Button overflow fix

Conflict resolution:
- Keep Haven Code fork identity (Bedrock settings, enableCodexProvider,
  claude-haiku-4-5 as default git text gen model, Bedrock badge in picker)
- Accept upstream refactors (draftsByThreadId variable pattern,
  resolveModelOptions callback, CSS improvements)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@cramt
Copy link

cramt commented Mar 18, 2026

Im not sure if this is ready for reviews and such yet, but these errors when testing it out. On NixOS btw

Error: claudeAgent adapter thread is closed: 09275974-8eaf-4fae-9883-182504e8c13c
    at toSessionError$1 (file:///nix/store/xqwhpgd12879iq7jjzin7m29nq3f55xh-t3code-0.0.10-claude/lib/t3code/apps/server/dist/index.mjs:84100:44)
    at toRequestError$1 (file:///nix/store/xqwhpgd12879iq7jjzin7m29nq3f55xh-t3code-0.0.10-claude/lib/t3code/apps/server/dist/index.mjs:84107:23)
    at catch (file:///nix/store/xqwhpgd12879iq7jjzin7m29nq3f55xh-t3code-0.0.10-claude/lib/t3code/apps/server/dist/index.mjs:85360:23)
    at file:///nix/store/xqwhpgd12879iq7jjzin7m29nq3f55xh-t3code-0.0.10-claude/lib/t3code/apps/server/dist/index.mjs:8830:108 {
  [cause]: Error: Query closed before response received
      at g9.cleanup (file:///nix/store/xqwhpgd12879iq7jjzin7m29nq3f55xh-t3code-0.0.10-claude/lib/t3code/apps/server/dist/index.mjs:76388:12)
      at g9.readMessages (file:///nix/store/xqwhpgd12879iq7jjzin7m29nq3f55xh-t3code-0.0.10-claude/lib/t3code/apps/server/dist/index.mjs:76445:36)
      at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
}
Error: Provider adapter request failed (claudeAgent) for turn/setModel: ProcessTransport is not ready for writing
    at toRequestError$1 (file:///nix/store/xqwhpgd12879iq7jjzin7m29nq3f55xh-t3code-0.0.10-claude/lib/t3code/apps/server/dist/index.mjs:84109:9)
    at catch (file:///nix/store/xqwhpgd12879iq7jjzin7m29nq3f55xh-t3code-0.0.10-claude/lib/t3code/apps/server/dist/index.mjs:85360:23)
    at file:///nix/store/xqwhpgd12879iq7jjzin7m29nq3f55xh-t3code-0.0.10-claude/lib/t3code/apps/server/dist/index.mjs:8830:108 {
  [cause]: Error: ProcessTransport is not ready for writing
      at x9.write (file:///nix/store/xqwhpgd12879iq7jjzin7m29nq3f55xh-t3code-0.0.10-claude/lib/t3code/apps/server/dist/index.mjs:76157:48)
      at file:///nix/store/xqwhpgd12879iq7jjzin7m29nq3f55xh-t3code-0.0.10-claude/lib/t3code/apps/server/dist/index.mjs:76636:39
      at new Promise (<anonymous>)
      at g9.request (file:///nix/store/xqwhpgd12879iq7jjzin7m29nq3f55xh-t3code-0.0.10-claude/lib/t3code/apps/server/dist/index.mjs:76629:10)
      at g9.setModel (file:///nix/store/xqwhpgd12879iq7jjzin7m29nq3f55xh-t3code-0.0.10-claude/lib/t3code/apps/server/dist/index.mjs:76568:14)
      at try (file:///nix/store/xqwhpgd12879iq7jjzin7m29nq3f55xh-t3code-0.0.10-claude/lib/t3code/apps/server/dist/index.mjs:85359:30)
      at file:///nix/store/xqwhpgd12879iq7jjzin7m29nq3f55xh-t3code-0.0.10-claude/lib/t3code/apps/server/dist/index.mjs:8830:23
}

juliusmarminge and others added 2 commits March 18, 2026 13:25
Co-authored-by: codex <codex@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.