Skip to content

Upstream sync: Claude adapter + plan consumption + fixes (v0.0.13)#26

Merged
aaditagrawal merged 15 commits intomainfrom
upstream-sync-claude-agent
Mar 20, 2026
Merged

Upstream sync: Claude adapter + plan consumption + fixes (v0.0.13)#26
aaditagrawal merged 15 commits intomainfrom
upstream-sync-claude-agent

Conversation

@aaditagrawal
Copy link
Owner

@aaditagrawal aaditagrawal commented Mar 20, 2026

Summary

Integrates 6 upstream commits from pingdotgg/t3code into our multi-provider fork, preserving all 8 providers (codex, copilot, claudeAgent, cursor, opencode, geminiCli, amp, kilo).

Upstream commits cherry-picked:

Strategy

  • Preparatory rename: claudeCodeclaudeAgent to align naming before cherry-picks, reducing conflict surface
  • Cherry-pick per commit instead of git merge upstream/main to isolate conflicts
  • Multi-provider preservation: Every upstream 2-provider conditional was extended to our 8 providers

Key changes

  • New Claude adapter (ClaudeAdapter.ts, 2900+ lines) with SDK integration, fast mode, ultrathink, adaptive reasoning
  • Plan consumption: Users can now implement proposed plans, with cross-thread plan tracking
  • modelOptions consolidation: Replaced per-field draft state (effort, codexFastMode, claudeAgentEffort) with unified ProviderModelOptions object
  • New UI: ClaudeTraitsPicker, CompactComposerControlsMenu, ultrathink visual effects
  • SDK type declarations expanded to match upstream's usage patterns
  • Provider capabilities registry integrated into upstream's Claude adapter

Verification

  • bun typecheck — 7/7 packages pass
  • bun lint — 0 errors, 10 warnings (unused imports from merge)

Test plan

  • bun typecheck passes all 7 packages
  • bun lint has no errors
  • bun test apps/server/src/provider/ — adapter tests
  • bun test apps/server/src/orchestration/ — orchestration tests
  • bun test packages/contracts/ — contract tests
  • bun test packages/shared/ — shared model tests
  • Manual browser check for ChatView changes
  • Verify Claude adapter starts sessions correctly
  • Verify existing providers (codex, copilot, cursor, etc.) still work

Summary by CodeRabbit

  • New Features

    • Claude Agent added across UI: provider/model pickers, composer trait controls (effort, fast-mode, thinking, ultrathink) and animations; ultrathink-aware prompt formatting.
    • Proposed-plan linking: plans show implemented status and actionable sidebar behavior.
    • Per-thread model options and improved provider-aware model selection in composer.
  • Bug Fixes

    • Clearer stale-approval and stale user-input failure messages; more robust session resume/restart behavior.
  • Chores

    • Package/version bumps, DB migrations, expanded tests and probes.

aaditagrawal and others added 7 commits March 20, 2026 10:00
Preparatory rename for upstream sync. Upstream renamed the provider kind
from "claudeCode" to "claudeAgent" and type names accordingly. This
rename reduces future sync friction.

- Provider kind: "claudeCode" → "claudeAgent"
- ClaudeCodeProviderStartOptions → ClaudeProviderStartOptions
- ClaudeCodeModelOptions → ClaudeModelOptions
- ClaudeCodeAdapter → ClaudeAdapter (files + class)
- ClaudeCodeTraitsPicker → ClaudeTraitsPicker
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: JustYannicc <52761674+JustYannicc@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Renames Claude provider from claudeCodeclaudeAgent, adds Claude model options (effort/ultrathink, fastMode, thinking), introduces ClaudeAdapter service and SDK typings/tests, persists and forwards modelOptions, adds proposed-plan provenance/implementation fields with migrations, and updates server ingestion, projections, and web UI/store accordingly.

Changes

Cohort / File(s) Summary
Provider renames & contracts
packages/contracts/src/..., packages/shared/src/model.ts, packages/contracts/package.json
Change provider identifier claudeCodeclaudeAgent across contracts/types, widen model-options schema (ClaudeModelOptions / fastMode / ultrathink), add reasoning-effort options and inferProviderForModel helpers; bump contracts package version.
Adapter services & registry
apps/server/src/provider/Services/ClaudeAdapter.ts, apps/server/src/provider/Services/ClaudeCodeAdapter.ts, apps/server/src/provider/Services/ProviderAdapter.ts, apps/server/src/provider/Layers/ProviderAdapterRegistry.ts, apps/server/src/serverLayers.ts
Remove legacy ClaudeCode adapter service, add new ClaudeAdapter service/tag and rewire registry/server layer to register claudeAgent adapter.
Claude SDK & adapter tests / probes
apps/server/src/provider/claude-agent-sdk.d.ts, apps/server/src/provider/Layers/ClaudeAdapter.test.ts, apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts (deleted), apps/server/src/provider/Layers/ClaudeSdkFastMode.probe.test.ts, scripts/*.ts, scripts/package.json
Add/extend SDK typings, introduce comprehensive ClaudeAdapter tests and fast-mode probes, remove legacy ClaudeCode tests, add CLI probe scripts and bump @anthropic-ai/claude-agent-sdk.
Provider runtime, session persistence & health
apps/server/src/provider/Layers/ProviderService.ts, apps/server/src/provider/Layers/ProviderService.test.ts, apps/server/src/provider/Layers/ProviderHealth.ts, apps/server/src/provider/Layers/ProviderHealth.test.ts, apps/server/src/wsServer.ts
Persist/restore modelOptions, forward resumeCursor/last runtime events, add Claude CLI health checks (claudeAgent), adjust WS usage handler to claudeAgent.
Orchestration ingestion & reactors
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts, .../ProviderRuntimeIngestion.test.ts, ProviderCommandReactor.ts, ProviderCommandReactor.test.ts, decider.ts
Track per-thread modelOptions, restart Claude sessions when modelOptions change, mark source proposed plans implemented when matching turns start, improve failure/activity formatting, and validate/propagate sourceProposedPlan on turn-start.
Projections, persistence & migrations
apps/server/src/persistence/Services/ProjectionThreadProposedPlans.ts, .../ProjectionTurns.ts, .../Migrations/*.ts, apps/server/src/orchestration/Layers/ProjectionPipeline.ts, ProjectionSnapshotQuery.ts
Add implementedAt/implementationThreadId to proposed-plan rows, add source_proposed_plan_* fields on turns, update repositories/queries, and add two migrations (014/015) plus projection updates.
Server manager & collab routing
apps/server/src/codexAppServerManager.ts, apps/server/src/codexAppServerManager.test.ts
Add collabReceiverTurns tracking, suppress/route child-conversation notifications to parent turnId, and add tests for collab child routing and notification behaviors.
Provider adapter runtime & tests
apps/server/src/provider/Layers/ProviderAdapterConformance.test.ts, ProviderAdapterRegistry.test.ts, ProviderService.test.ts, ProviderSessionDirectory.ts
Update tests/registry to use claudeAgent and new adapter shape; adjust fake adapter/session test behaviors and resumeCursor naming.
Web app: model picker, composer, traits & UI
apps/web/src/appSettings.ts, appSettings.test.ts, composerDraftStore.ts, composerDraftStore.test.ts, components/**/ClaudeTraitsPicker*.tsx, CodexTraitsPicker*.tsx, ProviderModelPicker*.tsx, ChatView.tsx, Sidebar*.tsx, ProviderLogo.tsx, index.css
Migrate persisted composer drafts to unified modelOptions, add ClaudeTraitsPicker (ultrathink/fastMode/thinking), remap UI to claudeAgent, apply ultrathink prompt prefixing and CSS animations, and include proposed-plan fields in UI state and snapshots.
Session, timeline & worklog logic
apps/web/src/session-logic.ts, session-logic.test.ts, apps/web/src/store.ts, types.ts
Add sourceProposedPlan usage, include implementedAt/implementationThreadId in proposed-plan types/state, introduce findSidebarProposedPlan and hasActionableProposedPlan, adjust timeline/work-log collapsing and stale pending request handling.
Persistence & storage utilities (web)
apps/web/src/lib/storage.ts, apps/web/src/lib/utils.ts
Add generic StateStorage/DebouncedStorage implementations and isLinuxPlatform helper used by desktop browse behavior.
Many tests & snapshots
**/*.test.ts, **/*.browser.tsx across server & web
Widespread test updates: rename claudeCodeclaudeAgent, include proposed-plan fields in fixtures/snapshots, add probe/adapter tests, remove legacy ClaudeCode test modules.
Version bumps & CI
apps/desktop/package.json, apps/server/package.json, apps/web/package.json, .github/workflows/release.yml, apps/web/public/mockServiceWorker.js, apps/web/vitest.browser.config.ts
Bump package versions, upgrade Anthropic SDK, change release job to read secrets.RELEASE_APP_ID, update MSW version and Vitest browser includes.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant UI as Composer UI
    participant Store as Draft Store
    participant Server as Orchestration Server
    participant Adapter as ClaudeAdapter
    participant SDK as Claude SDK

    User->>UI: select model + set modelOptions (claudeAgent)
    UI->>Store: setModelOptions(threadId, { claudeAgent: {...} })
    UI->>Server: POST thread.turn.start (maybe sourceProposedPlan)
    Server->>Server: validate sourceProposedPlan & compute runtime inputs
    Server->>Adapter: ensureSessionForThread / startSession/sendTurn (modelOptions, resumeCursor)
    Adapter->>SDK: query({ effort, fastMode, thinking })
    SDK-->>Adapter: stream messages/events
    Adapter->>Server: emit ProviderRuntimeEvent (turn.started/completed, approvals, user-input, progress)
    Server->>Server: on turn.started -> mark source proposed plan implemented (implementedAt, implementationThreadId)
    Server-->>UI: updated turn + plan state
    UI->>User: render updated plan/turn status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Poem

🐰 I hopped through branches, nibbling code by night,

Claude now wears the name "claudeAgent" bright.
Ultrathink hums, fast-mode takes flight,
plans remember whence they came, marked right.
A soft thump — adapters stitched snug and tight!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch upstream-sync-claude-agent

@github-actions github-actions bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Mar 20, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
apps/server/src/provider/Layers/ProviderService.ts (2)

242-250: ⚠️ Potential issue | 🟠 Major

Persist modelOptions on every session rebind.

These branches call upsertSessionBinding(...) without extra.modelOptions. Since ProviderSession itself does not carry modelOptions, the rewritten runtimePayload drops them after the first recover/stop cycle, and the next resume falls back to default provider settings instead of the persisted Claude/Codex tuning.

Also applies to: 290-296, 594-599

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ProviderService.ts` around lines 242 - 250,
The upsertSessionBinding calls (e.g., the call around
readPersistedProviderOptions and input.binding.runtimePayload) omit passing
persisted modelOptions, causing ProviderSession to lose model tuning on rebind;
update all upsertSessionBinding calls (including the instances referenced and
similar calls at the other locations) to include extra.modelOptions populated
from the persisted source (e.g., readPersistedProviderOptions or a
readPersistedModelOptions helper) when present so that the call becomes
upsertSessionBinding(..., existingProviderOptions ? { providerOptions:
existingProviderOptions, modelOptions: existingProviderOptions.modelOptions } :
undefined) (or equivalent extraction of modelOptions) to ensure modelOptions
survive recover/stop cycles.

347-369: ⚠️ Potential issue | 🟠 Major

startSession restores the persisted cursor, but not the persisted model options.

When startSession(...) is called for an existing binding, only resumeCursor is recovered. If the caller omits modelOptions, the adapter restarts with defaults and this upsert immediately overwrites the stored options with undefined.

Suggested patch
         const input = {
           ...parsed,
           threadId,
           provider: parsed.provider ?? "codex",
         };
         const persistedBinding = Option.getOrUndefined(yield* directory.getBinding(threadId));
+        const persistedModelOptions =
+          persistedBinding?.provider === input.provider
+            ? readPersistedModelOptions(persistedBinding.runtimePayload)
+            : undefined;
+        const effectiveModelOptions = input.modelOptions ?? persistedModelOptions;
         const effectiveResumeCursor =
           input.resumeCursor ??
           (persistedBinding?.provider === input.provider
             ? persistedBinding.resumeCursor
             : undefined);
         const adapter = yield* registry.getByProvider(input.provider);
         const session = yield* adapter.startSession({
           ...input,
+          ...(effectiveModelOptions !== undefined
+            ? { modelOptions: effectiveModelOptions }
+            : {}),
           ...(effectiveResumeCursor !== undefined ? { resumeCursor: effectiveResumeCursor } : {}),
         });

         if (session.provider !== adapter.provider) {
           return yield* toValidationError(
@@
 
         yield* upsertSessionBinding(session, threadId, {
-          modelOptions: input.modelOptions,
+          modelOptions: effectiveModelOptions,
           providerOptions: input.providerOptions,
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ProviderService.ts` around lines 347 - 369,
The code restores only resumeCursor from persistedBinding but not
modelOptions/providerOptions, causing undefined to overwrite stored options;
update ProviderService.startSession to derive effectiveModelOptions and
effectiveProviderOptions (e.g., const effectiveModelOptions = input.modelOptions
?? persistedBinding?.modelOptions and similarly for providerOptions), pass those
effective values into adapter.startSession and into the upsertSessionBinding
call (use effectiveModelOptions/effectiveProviderOptions instead of
input.modelOptions/input.providerOptions) so existing persisted options are
preserved when the caller omits them.
apps/web/src/lib/threadProvider.ts (1)

5-14: ⚠️ Potential issue | 🟠 Major

Keep a legacy alias for claudeCode in provider parsing.

Dropping claudeCode from recognized provider names can break inference for pre-rename persisted session data. Add a compatibility mapping in toProviderKind.

🛠️ Suggested compatibility bridge
 export function toProviderKind(providerName: string | null | undefined): ProviderKind | null {
   if (!providerName) return null;
+  if (providerName === "claudeCode") return "claudeAgent";
   return PROVIDER_KIND_SET.has(providerName as ProviderKind)
     ? (providerName as ProviderKind)
     : null;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/lib/threadProvider.ts` around lines 5 - 14, Add a compatibility
mapping so legacy provider name "claudeCode" is treated as "claudeAgent" during
parsing: update the provider parsing function (toProviderKind) to normalize
incoming strings by mapping "claudeCode" → "claudeAgent" before validating
against PROVIDER_KINDS, ensuring old persisted session data still resolves to
the new ProviderKind; keep the rest of the validation logic unchanged so
unrecognized names still error/return undefined as before.
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts (1)

511-524: ⚠️ Potential issue | 🟠 Major

Don't bake an unsupported Claude model into this expectation.

This test now accepts claudeAgent starting with gpt-5-codex, but the provider's model list only includes Claude models. That masks a real provider/model mismatch instead of catching it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts` around
lines 511 - 524, The test expectation is asserting the model "gpt-5-codex" for a
claudeAgent which is invalid; update the assertion so the mocked startSession
call expects a Claude model (e.g., "claude-2" or whichever model is defined in
the provider's model list) instead of "gpt-5-codex" — locate the assertion
checking harness.startSession.mock.calls[0]?.[1] and replace the hard-coded
"gpt-5-codex" with the correct Claude model or a lookup from the provider config
so the test reflects a supported provider/model pair.
🧹 Nitpick comments (12)
apps/server/src/provider/Layers/ProviderService.test.ts (2)

587-608: Remove the duplicated claudeAgent routing test.

This block is identical to the test on Lines 524-545, including the same title. Keeping both only slows the suite and makes failures harder to attribute.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ProviderService.test.ts` around lines 587 -
608, The test duplicated the "routes explicit claudeAgent provider session
starts to the claude adapter" case; remove the redundant test block that calls
ProviderService.startSession with provider "claudeAgent" (the one asserting
routing.claude.startSession.mock.calls.length and validating startInput.cwd and
startPayload.provider) so only the original test remains; ensure you delete the
duplicate it.effect test entirely (including its Effect.gen closure and
assertions) and keep the other identical test unchanged.

873-887: Replace the fixed sleeps with an explicit readiness signal.

These waits still rely on timing instead of knowing when the PubSub subscription is live. On a loaded CI runner, 50ms is still enough to intermittently miss the first emitted event.

Also applies to: 912-912, 976-976

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ProviderService.test.ts` around lines 873 -
887, Replace the timing-based sleeps with an explicit readiness handshake:
change the test to await a promise that resolves when the PubSub subscription is
actually registered before calling fanout.codex.emit. Concretely, modify the
subscription setup used by ProviderService tests to return (or set) a "ready"
promise/resolve callback when the handler is attached, then in the test await
that readiness promise instead of yield* sleep(50) before invoking
fanout.codex.emit (and do the same for the other two occurrences).
packages/contracts/src/model.ts (1)

238-238: Keep the new git-model default out of packages/contracts.

DEFAULT_GIT_TEXT_GENERATION_MODEL adds runtime configuration to a package that is supposed to stay schema/type-only. This default should live in the consuming runtime package instead of being exported from contracts.

As per coding guidelines, "Keep packages/contracts package schema-only — include no runtime logic, only effect/Schema schemas and TypeScript contracts for provider events, WebSocket protocol, and model/session types."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/contracts/src/model.ts` at line 238, Remove the runtime default from
the contracts package: delete the exported constant
DEFAULT_GIT_TEXT_GENERATION_MODEL from packages/contracts/src/model.ts so
contracts remain schema/type-only, and instead define that default in the
consuming runtime package (where configuration/implementation lives). Update any
imports that currently reference DEFAULT_GIT_TEXT_GENERATION_MODEL to point to
the new runtime location (or to use a literal/default there) so
packages/contracts only contains types/schemas (e.g., leave model
types/interfaces in model.ts but move the actual string default into the runtime
codebase).
apps/server/src/wsServer.ts (1)

986-988: claudeAgent usage endpoint returns a stub - is this intentional?

The handler returns only { provider } without actual usage data. Other providers like copilot, codex, and cursor have full implementations. If the Claude Agent SDK provides usage/quota information, consider implementing it here for parity. If not available, the stub is acceptable but a brief comment would clarify intent.

📝 Optional: Add clarifying comment
         if (provider === "claudeAgent") {
+          // Claude Agent SDK does not expose usage metrics; return minimal stub
           return { provider } satisfies ProviderUsageResult;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/wsServer.ts` around lines 986 - 988, The branch handling
provider === "claudeAgent" returns only a stub ({ provider } satisfies
ProviderUsageResult) without usage/quota data; either implement actual usage
retrieval using the Claude Agent SDK (call its usage/quota methods and populate
the ProviderUsageResult fields like usage, quota, remaining, etc., then return
that object) inside the same conditional, or add a clear inline comment above
the clause explaining that the Claude Agent SDK does not expose usage/quota and
this stub is intentional; update the return to match the shape returned for
other providers (e.g., copilot, codex, cursor) so callers receive a consistent
ProviderUsageResult.
apps/web/src/appSettings.test.ts (1)

209-215: Remove duplicated provider-custom-model test block.

This assertion is already covered in Line [173]-Line [179]. Keeping both adds redundant maintenance without extra signal.

♻️ Suggested cleanup
-describe("provider-specific custom models", () => {
-  it("includes provider-specific custom slugs in non-codex model lists", () => {
-    const claudeOptions = getAppModelOptions("claudeAgent", ["claude/custom-opus"]);
-
-    expect(claudeOptions.some((option) => option.slug === "claude/custom-opus")).toBe(true);
-  });
-});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/appSettings.test.ts` around lines 209 - 215, Remove the
duplicate test block that asserts provider-specific custom slugs are included:
delete the describe/it block calling getAppModelOptions("claudeAgent",
["claude/custom-opus"]) and its expect assertion because the same assertion is
already covered earlier; ensure no other unique assertions in that block are
lost and run tests after removal to confirm coverage (look for
getAppModelOptions and the "provider-specific custom models" test title to
locate the duplicate).
apps/web/src/index.css (1)

421-482: Add reduced-motion fallback for animated ultrathink styles.

These infinite animations should be disabled for users with prefers-reduced-motion.

♿ Suggested accessibility fallback
 `@keyframes` ultrathink-chroma-shift {
@@
 }
 
+@media (prefers-reduced-motion: reduce) {
+  .ultrathink-frame,
+  .ultrathink-chroma,
+  .ultrathink-pill,
+  .ultrathink-word {
+    animation: none !important;
+  }
+}
+
 .ultrathink-frame {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/index.css` around lines 421 - 482, Add a prefers-reduced-motion
fallback that disables the infinite animations and motion-based effects for the
ultrathink styles: inside a `@media` (prefers-reduced-motion: reduce) block
override the animations for ultrathink-rainbow and ultrathink-chroma (and any
animated properties on .ultrathink-frame, .ultrathink-chroma, .ultrathink-pill,
.ultrathink-word) to none (or effectively zero duration) and freeze animated
properties like background-position and filter to a sensible static value so the
gradient/chroma remain visible but not animated.
apps/web/src/components/chat/ProviderModelPicker.tsx (2)

180-186: Unused type guard can be removed.

The isAvailableProviderOption type guard is defined but never called in this file. Consider removing it to reduce dead code, or use it to filter AVAILABLE_PROVIDER_OPTIONS if type narrowing is needed elsewhere.

🧹 Suggested removal
-function isAvailableProviderOption(option: (typeof PROVIDER_OPTIONS)[number]): option is {
-  value: ProviderKind;
-  label: string;
-  available: true;
-} {
-  return option.available;
-}
-
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/ProviderModelPicker.tsx` around lines 180 - 186,
The function isAvailableProviderOption is an unused type guard; remove this dead
code or use it when constructing AVAILABLE_PROVIDER_OPTIONS to narrow types.
Either delete the isAvailableProviderOption function definition entirely (and
remove any imports referencing it) or apply it to filter PROVIDER_OPTIONS into
AVAILABLE_PROVIDER_OPTIONS so the resulting array is correctly typed; update any
usages of AVAILABLE_PROVIDER_OPTIONS (and references to PROVIDER_OPTIONS) to
reflect the narrowed type if you choose to keep the guard.

269-280: Unused handleModelChange function can be removed.

This function is defined but never called. The component uses inline onModelSelect callbacks within the map iterations instead. Consider removing to eliminate dead code.

🧹 Suggested removal
-  const handleModelChange = (provider: ProviderKind, value: string) => {
-    if (props.disabled) return;
-    if (!value) return;
-    const resolvedModel = resolveModelForProviderPicker(
-      provider,
-      value,
-      props.modelOptionsByProvider[provider],
-    );
-    if (!resolvedModel) return;
-    props.onProviderModelChange(provider, resolvedModel);
-    setIsMenuOpen(false);
-  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/ProviderModelPicker.tsx` around lines 269 - 280,
Remove the dead helper function handleModelChange since it is defined but never
used; locate the function named handleModelChange in ProviderModelPicker and
delete it along with any unused imports/closures it references (e.g.,
resolveModelForProviderPicker, props.onProviderModelChange, setIsMenuOpen) or,
if you prefer to keep behavior, replace its single-use callers with calls to the
existing inline onModelSelect handlers instead; ensure no references to
handleModelChange remain and run type checks to remove any leftover unused
symbol warnings.
apps/server/src/provider/Layers/ProviderHealth.ts (1)

282-301: Consider extracting a shared command runner to reduce duplication.

runClaudeCommand is nearly identical to runCodexCommand (lines 261-280) and runGeminiCommand (lines 431-450). While the duplication is acceptable for clarity, a parameterized helper could reduce maintenance burden.

♻️ Optional: Extract shared command runner
const makeCommandRunner = (binary: string) => (args: ReadonlyArray<string>) =>
  Effect.gen(function* () {
    const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
    const command = ChildProcess.make(binary, [...args], {
      shell: process.platform === "win32",
    });
    const child = yield* spawner.spawn(command);
    const [stdout, stderr, exitCode] = yield* Effect.all(
      [
        collectStreamAsString(child.stdout),
        collectStreamAsString(child.stderr),
        child.exitCode.pipe(Effect.map(Number)),
      ],
      { concurrency: "unbounded" },
    );
    return { stdout, stderr, code: exitCode } satisfies CommandResult;
  }).pipe(Effect.scoped);

const runCodexCommand = makeCommandRunner("codex");
const runClaudeCommand = makeCommandRunner("claude");
const runGeminiCommand = makeCommandRunner("gemini");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ProviderHealth.ts` around lines 282 - 301,
Extract the duplicated child-process logic into a parameterized helper (e.g.,
makeCommandRunner) and replace runCodexCommand, runClaudeCommand, and
runGeminiCommand to call that helper with their respective binary names; the
helper should encapsulate the Effect.gen block that obtains
ChildProcessSpawner.ChildProcessSpawner, constructs ChildProcess.make(binary,
[...args], { shell: process.platform === "win32" }), spawns the child, collects
stdout/stderr via collectStreamAsString, maps child.exitCode to Number, and
returns the CommandResult, then .pipe(Effect.scoped) so the three run* functions
simply reference makeCommandRunner("codex"/"claude"/"gemini").
.plans/17-claude-agent.md (1)

68-70: Consider aligning providerOptions.claudeCode naming with claudeAgent.

The plan references providerOptions.claudeCode as the provider-specific options key. Given the PR's rename from claudeCode to claudeAgent throughout the codebase, consider whether the options key should also be providerOptions.claudeAgent for consistency.

However, if claudeCode refers specifically to the Claude Code SDK/binary (distinct from the agent abstraction), keeping it may be intentional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.plans/17-claude-agent.md around lines 68 - 70, The plan uses
providerOptions.claudeCode but the codebase renamed claudeCode to claudeAgent;
update the plan and types to use providerOptions.claudeAgent for consistency by
replacing all occurrences of providerOptions.claudeCode with
providerOptions.claudeAgent and updating the corresponding ProviderOptions/typed
providerOptions interface and any usages in functions or classes that read that
key (e.g., code that constructs the Claude agent, references to
claudeAgent/ClaudeAgent), unless the original providerOptions.claudeCode
intentionally refers to the separate Claude Code SDK—in that exceptional case
add a clarifying comment and keep the original name.
apps/web/src/session-logic.test.ts (1)

449-543: Add the cross-thread source-plan case here.

findSidebarProposedPlan() now resolves latestTurn.sourceProposedPlan.threadId against the full threads collection, but these fixtures only cover the same-thread lookup. A regression in the actual cross-thread plan-consumption path would still pass this suite.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/session-logic.test.ts` around lines 449 - 543, Tests for
findSidebarProposedPlan only cover when latestTurn.sourceProposedPlan points to
a plan in the same thread; add a case where
latestTurn.sourceProposedPlan.threadId refers to a different thread in the
threads array to ensure cross-thread lookup works. Update the test suite (the
describe "findSidebarProposedPlan" block) to include a scenario where
latestTurn.sourceProposedPlan.threadId is ThreadId.makeUnsafe("thread-2") while
the current threadId is "thread-1", populate proposedPlans on thread-2 with the
referenced plan, and assert the returned plan matches that cross-thread plan so
the implementation that resolves sourceProposedPlan.threadId against the full
threads collection is exercised.
apps/server/src/provider/Layers/ClaudeAdapter.test.ts (1)

1075-1101: Scope this stream consumer to the test fiber.

Effect.runFork(...) here creates an unscoped reader, so if anything fails before Line 1100 it can keep draining adapter.streamEvents into later tests. The fixed yieldNow chain also makes the assertion timing-sensitive. Prefer Effect.forkChild plus Fiber.interrupt so cleanup stays tied to the test scope.

♻️ Suggested cleanup pattern
-      const runtimeEventsFiber = Effect.runFork(
-        Stream.runForEach(adapter.streamEvents, (event) =>
-          Effect.sync(() => {
-            runtimeEvents.push(event);
-          }),
-        ),
-      );
+      const runtimeEventsFiber = yield* Stream.runForEach(adapter.streamEvents, (event) =>
+        Effect.sync(() => {
+          runtimeEvents.push(event);
+        }),
+      ).pipe(Effect.forkChild);
...
-      runtimeEventsFiber.interruptUnsafe();
+      yield* Fiber.interrupt(runtimeEventsFiber);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ClaudeAdapter.test.ts` around lines 1075 -
1101, Replace the unscoped Effect.runFork consumer with a child fiber scoped to
the test: use Effect.forkChild (or the equivalent scoped fork) to start
Stream.runForEach(adapter.streamEvents, ...) and save the returned child Fiber
(currently runtimeEventsFiber) so it is bound to the test fiber; when cleaning
up interrupt that child fiber with its interrupt method (not interruptUnsafe) so
the consumer cannot outlive the test and race with later tests; keep the
Stream.runForEach and runtimeEvents push logic unchanged but swap the fork call
and cleanup to use forkChild + Fiber.interrupt on runtimeEventsFiber.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/orchestration/Layers/ProviderCommandReactor.ts`:
- Line 166: threadModelOptions is populated but never cleared on thread
teardown; update the thread teardown handlers that already remove
threadProviderOptions (those reacting to "thread.deleted" and
"thread.session-stop-requested") to also delete the entry from
threadModelOptions. Locate the Map named threadModelOptions and add
corresponding threadModelOptions.delete(threadId) calls inside the same event
handlers (and mirror the same cleanup logic referenced around the other
occurrence at the block noted by the reviewer) so option blobs are removed when
a thread is torn down.
- Around line 540-550: The catch currently maps all causes (including
interrupts) to a provider failure activity; instead, detect and re-raise
interruption causes so shutdown/drain interrupts are preserved and only
non-interrupt failures produce the appendProviderFailureActivity call. Modify
the Effect.catchCause block around the pipeline that calls
appendProviderFailureActivity (the block referencing event.payload.threadId,
kind "provider.turn.start.failed", and appendProviderFailureActivity) to inspect
the Cause and rethrow/propagate if it's an interrupt (preserve the cause) and
only call appendProviderFailureActivity for non-interrupt causes.

In `@apps/server/src/provider/Layers/CodexAdapter.ts`:
- Around line 110-116: The helpers toTurnId and toProviderItemId check trimmed
input but pass the original untrimmed value into TurnId.makeUnsafe and
ProviderItemId.makeUnsafe, which preserves whitespace in IDs; update both
functions to trim once into a local variable (e.g., const v = value?.trim()) and
use that trimmed v for the truthy check and when calling TurnId.makeUnsafe(v)
and ProviderItemId.makeUnsafe(v) so IDs never include leading/trailing
whitespace.

In `@apps/web/src/appSettings.ts`:
- Around line 49-58: Persisted values under
gitTextGenerationModelByProvider.claudeCode will be dropped because
PROVIDER_KINDS no longer includes "claudeCode"; add a migration step before
schema normalization that detects gitTextGenerationModelByProvider.claudeCode,
moves/merges its value to the new key (claudeAgent) and removes the legacy key
so users don't lose settings. Locate the normalization flow around
PROVIDER_KINDS and the code that reads/writes gitTextGenerationModelByProvider
and inject a one-time migration that checks for the legacy property, transfers
its value to gitTextGenerationModelByProvider.claudeAgent (respecting existing
claudeAgent value if present), and then deletes
gitTextGenerationModelByProvider.claudeCode.

In `@apps/web/src/components/chat/ProviderModelPicker.browser.tsx`:
- Around line 80-95: The test assumes ProviderModelPicker hides other provider
submenus when lockedProvider is set, but the component currently renders
submenus and only disables non-locked providers; update the test instead of the
component: in the "shows models directly when the provider is locked mid-thread"
spec (the mountPicker invocation and subsequent UI assertions), replace the
assertion that "Codex" is not present with assertions that "Codex" appears in
the popup but is disabled (check for a disabled attribute, aria-disabled, or a
CSS disabled class on the Codex menu item/button). Ensure the test still
verifies the locked provider's models (e.g., "Claude Sonnet 4.6") are visible
and interactable while other providers exist but are disabled.

In `@apps/web/src/composerDraftStore.ts`:
- Around line 335-407: The normalizeProviderModelOptions function reconstructs
and returns an object containing only codex and claudeAgent, which drops other
provider keys (copilot, cursor, opencode, geminiCli, amp, kilo, etc.); update
normalizeProviderModelOptions to merge and preserve any other valid provider
entries from the incoming candidate (or the original value) into the returned
ProviderModelOptions instead of rebuilding only codex/claudeAgent — locate
normalizeProviderModelOptions and the similar reconstruction sites (the other
occurrences noted around the file) and change the return to include
...otherProviders (or otherwise copy through unknown provider keys that match
ProviderModelOptions shape) so existing provider fields are retained during
setModelOptions()/rehydration.
- Around line 34-37: The debounced storage (composerDebouncedStorage created via
createDebouncedStorage) delays setItem() and can cause
syncPersistedAttachments() to read a stale snapshot; fix by flushing the
debounced store before any synchronous read of persisted attachments: call and
await composerDebouncedStorage.flush() (or the equivalent immediate-sync method
provided by createDebouncedStorage) at the start of syncPersistedAttachments()
and before the probe that checks persisted attachments (the code around
syncPersistedAttachments and the read at lines ~787-796), making the caller
async if needed so the flush completes before reading storage.

In `@apps/web/src/index.css`:
- Line 450: The CSS declaration background-image: var(--ultrathink-spectrum); in
index.css violates the declaration-empty-line-before rule; fix it by adding a
single empty line immediately above that declaration (or otherwise ensure it is
the first declaration in the rule) so the block conforms to Stylelint's
declaration-empty-line-before expectations in apps/web/src/index.css.

In `@apps/web/src/session-logic.ts`:
- Around line 541-570: The current collapse logic in
shouldCollapseToolLifecycleEntries (used by collapseDerivedWorkLogEntries and
mergeDerivedWorkLogEntries) only compares collapseKey, causing distinct adjacent
tool calls with identical args to be merged; modify
shouldCollapseToolLifecycleEntries so it also requires a stable instance
discriminator to match (preferably activity.providerItemId if present, otherwise
activity.turnId) before returning true, i.e., only collapse when collapseKey is
equal AND the chosen instance id is equal between previous and next; update any
callers/tests referencing this behavior accordingly.

In `@apps/web/src/store.ts`:
- Around line 9-13: The import list includes an unused symbol
inferProviderForModel; remove inferProviderForModel from the import statement so
only resolveModelSlug and resolveModelSlugForProvider remain imported (update
the import that currently references inferProviderForModel to exclude it).

In `@packages/contracts/src/providerRuntime.ts`:
- Around line 431-433: The multiSelect field currently declares multiSelect:
Schema.optional(Schema.Boolean) but uses Schema.withConstructorDefault(() =>
Option.some(false)), causing a type mismatch between Boolean|undefined and
Option<boolean>; fix by either changing the schema to an Option type with
Schema.optionalWith(Schema.Boolean, { as: "Option" }) and keep
Schema.withConstructorDefault(() => Option.some(false)), or keep
Schema.optional(Schema.Boolean) and replace the default with a plain boolean via
Schema.withConstructorDefault(() => false); update the multiSelect declaration
and its .pipe(...) usage accordingly (referencing multiSelect, Schema.optional,
Schema.optionalWith, Schema.withConstructorDefault, and Option.some).

In `@packages/shared/src/model.test.ts`:
- Around line 74-79: Remove the duplicated test block named "uses
provider-specific aliases" that calls normalizeModelSlug (the assertions
expecting "claude-sonnet-4-6", "claude-opus-4-6", and "claude-haiku-4-5")—keep
the original single test and delete the redundant copy so there is only one set
of these assertions in model.test.ts.

In `@packages/shared/src/model.ts`:
- Around line 466-480: inferProviderForModel currently only checks "claudeAgent"
and "codex", causing many provider-specific slugs to fall back; update
inferProviderForModel to consult MODEL_SLUG_SET_BY_PROVIDER for all providers
instead of only those two. Use normalizeModelSlug(model, <providerKind>) and
test membership in MODEL_SLUG_SET_BY_PROVIDER[<providerKind>] for each
ProviderKind (or iterate the keys of MODEL_SLUG_SET_BY_PROVIDER) and return the
matching provider, falling back to the existing final heuristic (and fallback
param) only if no provider matches; reference inferProviderForModel,
normalizeModelSlug, MODEL_SLUG_SET_BY_PROVIDER and ProviderKind to locate and
implement the change.
- Around line 610-624: The guard that prevents double-prepending the Ultrathink
prefix is case-sensitive and only checks for "Ultrathink:"; update
applyClaudePromptEffortPrefix to check for the prefix case-insensitively (e.g.,
use trimmed.toLowerCase().startsWith("ultrathink:") or a case-insensitive regex
test against trimmed) so inputs like "ultrathink:" or "ULTRATHINK:" are
recognized and the function won't add a second "Ultrathink:" prefix. Ensure you
replace the existing startsWith("Ultrathink:") check and keep the rest of the
logic (trim, effort === "ultrathink", return original trimmed string) intact.
- Around line 508-514: The overloads for getDefaultReasoningEffort promise a
non-null ProviderReasoningEffort for the generic provider signature but the
implementation can return null (via DEFAULT_REASONING_EFFORT_BY_PROVIDER) for
several ProviderKind values; update the third overload declaration (the one
taking provider?: ProviderKind) to return ProviderReasoningEffort | null so the
type matches runtime behavior, and keep the implementation of
getDefaultReasoningEffort(provider: ProviderKind = "codex") returning
DEFAULT_REASONING_EFFORT_BY_PROVIDER[provider] unchanged; reference
getDefaultReasoningEffort and DEFAULT_REASONING_EFFORT_BY_PROVIDER when making
the signature change.

In `@scripts/claude-fast-mode-probe.ts`:
- Around line 107-112: The non-success branch unconditionally calls
message.errors.join(...) which can throw because message.errors is optional;
update the result handling inside the message.type === "result" block (where
summary.resultSubtype, summary.resultFastModeState, and summary.resultText are
set) to guard the error path by using a safe fallback like
message.errors?.join("\n") ?? String(message.error ?? "") (or a similar fallback
such as JSON.stringify(message) or null) so summary.resultText is assigned
without throwing when errors is absent.

In `@scripts/claude-haiku-thinking-probe.ts`:
- Around line 228-241: When handling SDKMessage in the block that checks
message.type === "assistant" and message.type === "result", add defensive
null/undefined checks before accessing nested optional properties: ensure
message.message exists and message.message.content is an array before iterating
and incrementing summary.assistantThinkingBlockCount, and ensure message.result
and message.errors are present (fallback to empty string/array) before assigning
summary.resultText; also guard reading message.subtype into
summary.resultSubtype. Update the checks around the assistant loop and the
result branch (the code that references message.message.content,
message.subtype, message.result, and message.errors) so no property access
occurs on undefined.

---

Outside diff comments:
In `@apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts`:
- Around line 511-524: The test expectation is asserting the model "gpt-5-codex"
for a claudeAgent which is invalid; update the assertion so the mocked
startSession call expects a Claude model (e.g., "claude-2" or whichever model is
defined in the provider's model list) instead of "gpt-5-codex" — locate the
assertion checking harness.startSession.mock.calls[0]?.[1] and replace the
hard-coded "gpt-5-codex" with the correct Claude model or a lookup from the
provider config so the test reflects a supported provider/model pair.

In `@apps/server/src/provider/Layers/ProviderService.ts`:
- Around line 242-250: The upsertSessionBinding calls (e.g., the call around
readPersistedProviderOptions and input.binding.runtimePayload) omit passing
persisted modelOptions, causing ProviderSession to lose model tuning on rebind;
update all upsertSessionBinding calls (including the instances referenced and
similar calls at the other locations) to include extra.modelOptions populated
from the persisted source (e.g., readPersistedProviderOptions or a
readPersistedModelOptions helper) when present so that the call becomes
upsertSessionBinding(..., existingProviderOptions ? { providerOptions:
existingProviderOptions, modelOptions: existingProviderOptions.modelOptions } :
undefined) (or equivalent extraction of modelOptions) to ensure modelOptions
survive recover/stop cycles.
- Around line 347-369: The code restores only resumeCursor from persistedBinding
but not modelOptions/providerOptions, causing undefined to overwrite stored
options; update ProviderService.startSession to derive effectiveModelOptions and
effectiveProviderOptions (e.g., const effectiveModelOptions = input.modelOptions
?? persistedBinding?.modelOptions and similarly for providerOptions), pass those
effective values into adapter.startSession and into the upsertSessionBinding
call (use effectiveModelOptions/effectiveProviderOptions instead of
input.modelOptions/input.providerOptions) so existing persisted options are
preserved when the caller omits them.

In `@apps/web/src/lib/threadProvider.ts`:
- Around line 5-14: Add a compatibility mapping so legacy provider name
"claudeCode" is treated as "claudeAgent" during parsing: update the provider
parsing function (toProviderKind) to normalize incoming strings by mapping
"claudeCode" → "claudeAgent" before validating against PROVIDER_KINDS, ensuring
old persisted session data still resolves to the new ProviderKind; keep the rest
of the validation logic unchanged so unrecognized names still error/return
undefined as before.

---

Nitpick comments:
In @.plans/17-claude-agent.md:
- Around line 68-70: The plan uses providerOptions.claudeCode but the codebase
renamed claudeCode to claudeAgent; update the plan and types to use
providerOptions.claudeAgent for consistency by replacing all occurrences of
providerOptions.claudeCode with providerOptions.claudeAgent and updating the
corresponding ProviderOptions/typed providerOptions interface and any usages in
functions or classes that read that key (e.g., code that constructs the Claude
agent, references to claudeAgent/ClaudeAgent), unless the original
providerOptions.claudeCode intentionally refers to the separate Claude Code
SDK—in that exceptional case add a clarifying comment and keep the original
name.

In `@apps/server/src/provider/Layers/ClaudeAdapter.test.ts`:
- Around line 1075-1101: Replace the unscoped Effect.runFork consumer with a
child fiber scoped to the test: use Effect.forkChild (or the equivalent scoped
fork) to start Stream.runForEach(adapter.streamEvents, ...) and save the
returned child Fiber (currently runtimeEventsFiber) so it is bound to the test
fiber; when cleaning up interrupt that child fiber with its interrupt method
(not interruptUnsafe) so the consumer cannot outlive the test and race with
later tests; keep the Stream.runForEach and runtimeEvents push logic unchanged
but swap the fork call and cleanup to use forkChild + Fiber.interrupt on
runtimeEventsFiber.

In `@apps/server/src/provider/Layers/ProviderHealth.ts`:
- Around line 282-301: Extract the duplicated child-process logic into a
parameterized helper (e.g., makeCommandRunner) and replace runCodexCommand,
runClaudeCommand, and runGeminiCommand to call that helper with their respective
binary names; the helper should encapsulate the Effect.gen block that obtains
ChildProcessSpawner.ChildProcessSpawner, constructs ChildProcess.make(binary,
[...args], { shell: process.platform === "win32" }), spawns the child, collects
stdout/stderr via collectStreamAsString, maps child.exitCode to Number, and
returns the CommandResult, then .pipe(Effect.scoped) so the three run* functions
simply reference makeCommandRunner("codex"/"claude"/"gemini").

In `@apps/server/src/provider/Layers/ProviderService.test.ts`:
- Around line 587-608: The test duplicated the "routes explicit claudeAgent
provider session starts to the claude adapter" case; remove the redundant test
block that calls ProviderService.startSession with provider "claudeAgent" (the
one asserting routing.claude.startSession.mock.calls.length and validating
startInput.cwd and startPayload.provider) so only the original test remains;
ensure you delete the duplicate it.effect test entirely (including its
Effect.gen closure and assertions) and keep the other identical test unchanged.
- Around line 873-887: Replace the timing-based sleeps with an explicit
readiness handshake: change the test to await a promise that resolves when the
PubSub subscription is actually registered before calling fanout.codex.emit.
Concretely, modify the subscription setup used by ProviderService tests to
return (or set) a "ready" promise/resolve callback when the handler is attached,
then in the test await that readiness promise instead of yield* sleep(50) before
invoking fanout.codex.emit (and do the same for the other two occurrences).

In `@apps/server/src/wsServer.ts`:
- Around line 986-988: The branch handling provider === "claudeAgent" returns
only a stub ({ provider } satisfies ProviderUsageResult) without usage/quota
data; either implement actual usage retrieval using the Claude Agent SDK (call
its usage/quota methods and populate the ProviderUsageResult fields like usage,
quota, remaining, etc., then return that object) inside the same conditional, or
add a clear inline comment above the clause explaining that the Claude Agent SDK
does not expose usage/quota and this stub is intentional; update the return to
match the shape returned for other providers (e.g., copilot, codex, cursor) so
callers receive a consistent ProviderUsageResult.

In `@apps/web/src/appSettings.test.ts`:
- Around line 209-215: Remove the duplicate test block that asserts
provider-specific custom slugs are included: delete the describe/it block
calling getAppModelOptions("claudeAgent", ["claude/custom-opus"]) and its expect
assertion because the same assertion is already covered earlier; ensure no other
unique assertions in that block are lost and run tests after removal to confirm
coverage (look for getAppModelOptions and the "provider-specific custom models"
test title to locate the duplicate).

In `@apps/web/src/components/chat/ProviderModelPicker.tsx`:
- Around line 180-186: The function isAvailableProviderOption is an unused type
guard; remove this dead code or use it when constructing
AVAILABLE_PROVIDER_OPTIONS to narrow types. Either delete the
isAvailableProviderOption function definition entirely (and remove any imports
referencing it) or apply it to filter PROVIDER_OPTIONS into
AVAILABLE_PROVIDER_OPTIONS so the resulting array is correctly typed; update any
usages of AVAILABLE_PROVIDER_OPTIONS (and references to PROVIDER_OPTIONS) to
reflect the narrowed type if you choose to keep the guard.
- Around line 269-280: Remove the dead helper function handleModelChange since
it is defined but never used; locate the function named handleModelChange in
ProviderModelPicker and delete it along with any unused imports/closures it
references (e.g., resolveModelForProviderPicker, props.onProviderModelChange,
setIsMenuOpen) or, if you prefer to keep behavior, replace its single-use
callers with calls to the existing inline onModelSelect handlers instead; ensure
no references to handleModelChange remain and run type checks to remove any
leftover unused symbol warnings.

In `@apps/web/src/index.css`:
- Around line 421-482: Add a prefers-reduced-motion fallback that disables the
infinite animations and motion-based effects for the ultrathink styles: inside a
`@media` (prefers-reduced-motion: reduce) block override the animations for
ultrathink-rainbow and ultrathink-chroma (and any animated properties on
.ultrathink-frame, .ultrathink-chroma, .ultrathink-pill, .ultrathink-word) to
none (or effectively zero duration) and freeze animated properties like
background-position and filter to a sensible static value so the gradient/chroma
remain visible but not animated.

In `@apps/web/src/session-logic.test.ts`:
- Around line 449-543: Tests for findSidebarProposedPlan only cover when
latestTurn.sourceProposedPlan points to a plan in the same thread; add a case
where latestTurn.sourceProposedPlan.threadId refers to a different thread in the
threads array to ensure cross-thread lookup works. Update the test suite (the
describe "findSidebarProposedPlan" block) to include a scenario where
latestTurn.sourceProposedPlan.threadId is ThreadId.makeUnsafe("thread-2") while
the current threadId is "thread-1", populate proposedPlans on thread-2 with the
referenced plan, and assert the returned plan matches that cross-thread plan so
the implementation that resolves sourceProposedPlan.threadId against the full
threads collection is exercised.

In `@packages/contracts/src/model.ts`:
- Line 238: Remove the runtime default from the contracts package: delete the
exported constant DEFAULT_GIT_TEXT_GENERATION_MODEL from
packages/contracts/src/model.ts so contracts remain schema/type-only, and
instead define that default in the consuming runtime package (where
configuration/implementation lives). Update any imports that currently reference
DEFAULT_GIT_TEXT_GENERATION_MODEL to point to the new runtime location (or to
use a literal/default there) so packages/contracts only contains types/schemas
(e.g., leave model types/interfaces in model.ts but move the actual string
default into the runtime codebase).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f2401992-6971-4f73-8389-0ad80c7c1c48

📥 Commits

Reviewing files that changed from the base of the PR and between 36c98a6 and 487fd94.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (102)
  • .docs/provider-architecture.md
  • .github/workflows/release.yml
  • .plans/17-claude-agent.md
  • .plans/17-claude-code.md
  • AGENTS.md
  • apps/desktop/package.json
  • apps/server/integration/OrchestrationEngineHarness.integration.ts
  • apps/server/integration/TestProviderAdapter.integration.ts
  • apps/server/integration/orchestrationEngine.integration.test.ts
  • apps/server/package.json
  • apps/server/src/codexAppServerManager.test.ts
  • apps/server/src/codexAppServerManager.ts
  • apps/server/src/git/Layers/GitManager.test.ts
  • apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
  • apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts
  • apps/server/src/orchestration/Layers/ProjectionPipeline.ts
  • apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts
  • apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
  • apps/server/src/orchestration/decider.ts
  • apps/server/src/persistence/Layers/ProjectionThreadProposedPlans.ts
  • apps/server/src/persistence/Layers/ProjectionTurns.ts
  • apps/server/src/persistence/Migrations.ts
  • apps/server/src/persistence/Migrations/014_ProjectionThreadProposedPlanImplementation.ts
  • apps/server/src/persistence/Migrations/015_ProjectionTurnsSourceProposedPlan.ts
  • apps/server/src/persistence/Services/ProjectionThreadProposedPlans.ts
  • apps/server/src/persistence/Services/ProjectionTurns.ts
  • apps/server/src/provider/Layers/ClaudeAdapter.test.ts
  • apps/server/src/provider/Layers/ClaudeAdapter.ts
  • apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts
  • apps/server/src/provider/Layers/ClaudeSdkFastMode.probe.test.ts
  • apps/server/src/provider/Layers/CodexAdapter.test.ts
  • apps/server/src/provider/Layers/CodexAdapter.ts
  • apps/server/src/provider/Layers/ProviderAdapterConformance.test.ts
  • apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts
  • apps/server/src/provider/Layers/ProviderAdapterRegistry.ts
  • apps/server/src/provider/Layers/ProviderHealth.test.ts
  • apps/server/src/provider/Layers/ProviderHealth.ts
  • apps/server/src/provider/Layers/ProviderService.test.ts
  • apps/server/src/provider/Layers/ProviderService.ts
  • apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts
  • apps/server/src/provider/Layers/ProviderSessionDirectory.ts
  • apps/server/src/provider/Services/ClaudeAdapter.ts
  • apps/server/src/provider/Services/ClaudeCodeAdapter.ts
  • apps/server/src/provider/Services/ProviderAdapter.ts
  • apps/server/src/provider/Services/ProviderHealth.ts
  • apps/server/src/provider/claude-agent-sdk.d.ts
  • apps/server/src/serverLayers.ts
  • apps/server/src/wsServer.ts
  • apps/web/package.json
  • apps/web/public/mockServiceWorker.js
  • apps/web/src/appSettings.test.ts
  • apps/web/src/appSettings.ts
  • apps/web/src/components/BranchToolbar.logic.ts
  • apps/web/src/components/ChatView.browser.tsx
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/ProviderLogo.tsx
  • apps/web/src/components/Sidebar.logic.test.ts
  • apps/web/src/components/Sidebar.logic.ts
  • apps/web/src/components/Sidebar.tsx
  • apps/web/src/components/chat/ClaudeCodeTraitsPicker.tsx
  • apps/web/src/components/chat/ClaudeTraitsPicker.browser.tsx
  • apps/web/src/components/chat/ClaudeTraitsPicker.tsx
  • apps/web/src/components/chat/CodexTraitsPicker.browser.tsx
  • apps/web/src/components/chat/CodexTraitsPicker.tsx
  • apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx
  • apps/web/src/components/chat/CompactComposerControlsMenu.tsx
  • apps/web/src/components/chat/ProviderHealthBanner.tsx
  • apps/web/src/components/chat/ProviderModelPicker.browser.tsx
  • apps/web/src/components/chat/ProviderModelPicker.tsx
  • apps/web/src/composerDraftStore.test.ts
  • apps/web/src/composerDraftStore.ts
  • apps/web/src/index.css
  • apps/web/src/lib/storage.ts
  • apps/web/src/lib/threadDraftDefaults.test.ts
  • apps/web/src/lib/threadProvider.test.ts
  • apps/web/src/lib/threadProvider.ts
  • apps/web/src/lib/utils.ts
  • apps/web/src/routes/__root.tsx
  • apps/web/src/routes/_chat.settings.tsx
  • apps/web/src/session-logic.test.ts
  • apps/web/src/session-logic.ts
  • apps/web/src/store.test.ts
  • apps/web/src/store.ts
  • apps/web/src/types.ts
  • apps/web/vitest.browser.config.ts
  • packages/contracts/package.json
  • packages/contracts/src/model.ts
  • packages/contracts/src/orchestration.test.ts
  • packages/contracts/src/orchestration.ts
  • packages/contracts/src/provider.test.ts
  • packages/contracts/src/provider.ts
  • packages/contracts/src/providerRuntime.test.ts
  • packages/contracts/src/providerRuntime.ts
  • packages/shared/src/model.test.ts
  • packages/shared/src/model.ts
  • scripts/claude-fast-mode-probe.ts
  • scripts/claude-haiku-thinking-probe.ts
  • scripts/package.json
💤 Files with no reviewable changes (3)
  • apps/web/src/components/chat/ClaudeCodeTraitsPicker.tsx
  • apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts
  • apps/server/src/provider/Services/ClaudeCodeAdapter.ts

),
);

const threadModelOptions = new Map<ThreadId, ProviderModelOptions>();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clear threadModelOptions when a thread is torn down.

This cache is populated here, but unlike threadProviderOptions it is never deleted on thread.deleted or thread.session-stop-requested. Long-lived servers will retain option blobs for dead threads indefinitely.

Also applies to: 397-399

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/orchestration/Layers/ProviderCommandReactor.ts` at line 166,
threadModelOptions is populated but never cleared on thread teardown; update the
thread teardown handlers that already remove threadProviderOptions (those
reacting to "thread.deleted" and "thread.session-stop-requested") to also delete
the entry from threadModelOptions. Locate the Map named threadModelOptions and
add corresponding threadModelOptions.delete(threadId) calls inside the same
event handlers (and mirror the same cleanup logic referenced around the other
occurrence at the block noted by the reviewer) so option blobs are removed when
a thread is torn down.

Comment on lines +110 to +116
function toTurnId(value: string | undefined): TurnId | undefined {
return value?.trim() ? TurnId.makeUnsafe(value) : undefined;
}

function toProviderItemId(value: string | undefined): ProviderItemId | undefined {
return value?.trim() ? ProviderItemId.makeUnsafe(value) : undefined;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent use of trimmed value in ID helpers.

Both functions validate against the trimmed value (value?.trim()) but then create the ID with the original untrimmed value. If value contains leading/trailing whitespace, the check passes but the whitespace is preserved in the resulting ID.

🔧 Proposed fix to use trimmed value consistently
 function toTurnId(value: string | undefined): TurnId | undefined {
-  return value?.trim() ? TurnId.makeUnsafe(value) : undefined;
+  const trimmed = value?.trim();
+  return trimmed ? TurnId.makeUnsafe(trimmed) : undefined;
 }

 function toProviderItemId(value: string | undefined): ProviderItemId | undefined {
-  return value?.trim() ? ProviderItemId.makeUnsafe(value) : undefined;
+  const trimmed = value?.trim();
+  return trimmed ? ProviderItemId.makeUnsafe(trimmed) : undefined;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function toTurnId(value: string | undefined): TurnId | undefined {
return value?.trim() ? TurnId.makeUnsafe(value) : undefined;
}
function toProviderItemId(value: string | undefined): ProviderItemId | undefined {
return value?.trim() ? ProviderItemId.makeUnsafe(value) : undefined;
}
function toTurnId(value: string | undefined): TurnId | undefined {
const trimmed = value?.trim();
return trimmed ? TurnId.makeUnsafe(trimmed) : undefined;
}
function toProviderItemId(value: string | undefined): ProviderItemId | undefined {
const trimmed = value?.trim();
return trimmed ? ProviderItemId.makeUnsafe(trimmed) : undefined;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/CodexAdapter.ts` around lines 110 - 116, The
helpers toTurnId and toProviderItemId check trimmed input but pass the original
untrimmed value into TurnId.makeUnsafe and ProviderItemId.makeUnsafe, which
preserves whitespace in IDs; update both functions to trim once into a local
variable (e.g., const v = value?.trim()) and use that trimmed v for the truthy
check and when calling TurnId.makeUnsafe(v) and ProviderItemId.makeUnsafe(v) so
IDs never include leading/trailing whitespace.

Comment on lines +466 to +480
export function inferProviderForModel(
model: string | null | undefined,
fallback: ProviderKind = "codex",
): ProviderKind {
const normalizedClaude = normalizeModelSlug(model, "claudeAgent");
if (normalizedClaude && MODEL_SLUG_SET_BY_PROVIDER.claudeAgent.has(normalizedClaude)) {
return "claudeAgent";
}

const normalizedCodex = normalizeModelSlug(model, "codex");
if (normalizedCodex && MODEL_SLUG_SET_BY_PROVIDER.codex.has(normalizedCodex)) {
return "codex";
}

return typeof model === "string" && model.trim().startsWith("claude-") ? "claudeAgent" : fallback;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

inferProviderForModel() still ignores six providers.

Lines 466-480 only test the Claude Agent and Codex catalogs. Unique slugs like composer-1.5, smart, and gemini-2.5-pro therefore fall back instead of resolving to cursor, amp, and geminiCli, which makes this helper unsafe for the rest of the provider registry.

💡 Proposed fix
 export function inferProviderForModel(
   model: string | null | undefined,
   fallback: ProviderKind = "codex",
 ): ProviderKind {
-  const normalizedClaude = normalizeModelSlug(model, "claudeAgent");
-  if (normalizedClaude && MODEL_SLUG_SET_BY_PROVIDER.claudeAgent.has(normalizedClaude)) {
-    return "claudeAgent";
-  }
-
-  const normalizedCodex = normalizeModelSlug(model, "codex");
-  if (normalizedCodex && MODEL_SLUG_SET_BY_PROVIDER.codex.has(normalizedCodex)) {
-    return "codex";
-  }
-
-  return typeof model === "string" && model.trim().startsWith("claude-") ? "claudeAgent" : fallback;
+  const matches = (Object.keys(MODEL_SLUG_SET_BY_PROVIDER) as ProviderKind[]).filter((provider) => {
+    const normalized = normalizeModelSlug(model, provider);
+    return normalized ? MODEL_SLUG_SET_BY_PROVIDER[provider].has(normalized) : false;
+  });
+
+  if (matches.includes(fallback)) {
+    return fallback;
+  }
+  if (matches.length === 1) {
+    return matches[0];
+  }
+
+  return typeof model === "string" && model.trim().startsWith("claude-")
+    ? "claudeAgent"
+    : fallback;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/shared/src/model.ts` around lines 466 - 480, inferProviderForModel
currently only checks "claudeAgent" and "codex", causing many provider-specific
slugs to fall back; update inferProviderForModel to consult
MODEL_SLUG_SET_BY_PROVIDER for all providers instead of only those two. Use
normalizeModelSlug(model, <providerKind>) and test membership in
MODEL_SLUG_SET_BY_PROVIDER[<providerKind>] for each ProviderKind (or iterate the
keys of MODEL_SLUG_SET_BY_PROVIDER) and return the matching provider, falling
back to the existing final heuristic (and fallback param) only if no provider
matches; reference inferProviderForModel, normalizeModelSlug,
MODEL_SLUG_SET_BY_PROVIDER and ProviderKind to locate and implement the change.

Comment on lines +610 to +624
export function applyClaudePromptEffortPrefix(
text: string,
effort: ClaudeCodeEffort | null | undefined,
): string {
const trimmed = text.trim();
if (!trimmed) {
return trimmed;
}
if (effort !== "ultrathink") {
return trimmed;
}
if (trimmed.startsWith("Ultrathink:")) {
return trimmed;
}
return `Ultrathink:\n${trimmed}`;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make the ultrathink prefix guard case-insensitive.

Lines 621-624 only recognize Ultrathink: with exact casing. If a user edits the prompt to ultrathink: or ULTRATHINK:, this helper prepends a second copy.

💡 Proposed fix
-  if (trimmed.startsWith("Ultrathink:")) {
+  if (/^ultrathink:/i.test(trimmed)) {
     return trimmed;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/shared/src/model.ts` around lines 610 - 624, The guard that prevents
double-prepending the Ultrathink prefix is case-sensitive and only checks for
"Ultrathink:"; update applyClaudePromptEffortPrefix to check for the prefix
case-insensitively (e.g., use trimmed.toLowerCase().startsWith("ultrathink:") or
a case-insensitive regex test against trimmed) so inputs like "ultrathink:" or
"ULTRATHINK:" are recognized and the function won't add a second "Ultrathink:"
prefix. Ensure you replace the existing startsWith("Ultrathink:") check and keep
the rest of the logic (trim, effort === "ultrathink", return original trimmed
string) intact.

Comment on lines +107 to +112
if (message.type !== "result") {
return;
}
summary.resultSubtype = message.subtype;
summary.resultFastModeState = message.fast_mode_state ?? null;
summary.resultText = message.subtype === "success" ? message.result : message.errors.join("\n");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="$(fd 'claude-agent-sdk\.d\.ts$' | head -n1)"
sed -n '60,120p' "$file"

Repository: aaditagrawal/t3code

Length of output: 2016


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the actual file to confirm lines 107-112
echo "=== Lines 107-112 from scripts/claude-fast-mode-probe.ts ==="
cat -n scripts/claude-fast-mode-probe.ts | sed -n '105,115p'

echo ""
echo "=== Check for other message.errors usages in the file ==="
rg 'message\.errors' scripts/claude-fast-mode-probe.ts -A 2 -B 2

Repository: aaditagrawal/t3code

Length of output: 784


Guard the result error path when errors is absent.

Line 112 unconditionally calls message.errors.join("\n") for non-success results, but the SDK type defines errors as optional. This will throw if the SDK emits only the error field instead.

Suggested fix
  summary.resultSubtype = message.subtype;
  summary.resultFastModeState = message.fast_mode_state ?? null;
- summary.resultText = message.subtype === "success" ? message.result : message.errors.join("\n");
+ summary.resultText =
+   message.subtype === "success"
+     ? (message.result ?? null)
+     : Array.isArray(message.errors)
+       ? message.errors.map(String).join("\n")
+       : (typeof message.error === "string" ? message.error : null);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (message.type !== "result") {
return;
}
summary.resultSubtype = message.subtype;
summary.resultFastModeState = message.fast_mode_state ?? null;
summary.resultText = message.subtype === "success" ? message.result : message.errors.join("\n");
if (message.type !== "result") {
return;
}
summary.resultSubtype = message.subtype;
summary.resultFastModeState = message.fast_mode_state ?? null;
summary.resultText =
message.subtype === "success"
? (message.result ?? null)
: Array.isArray(message.errors)
? message.errors.map(String).join("\n")
: (typeof message.error === "string" ? message.error : null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/claude-fast-mode-probe.ts` around lines 107 - 112, The non-success
branch unconditionally calls message.errors.join(...) which can throw because
message.errors is optional; update the result handling inside the message.type
=== "result" block (where summary.resultSubtype, summary.resultFastModeState,
and summary.resultText are set) to guard the error path by using a safe fallback
like message.errors?.join("\n") ?? String(message.error ?? "") (or a similar
fallback such as JSON.stringify(message) or null) so summary.resultText is
assigned without throwing when errors is absent.

Comment on lines +228 to +241
if (message.type === "assistant") {
for (const block of message.message.content) {
if (typeof block?.type === "string" && block.type.includes("thinking")) {
summary.assistantThinkingBlockCount += 1;
}
}
return;
}

if (message.type === "result") {
summary.resultSubtype = message.subtype;
summary.resultText = message.subtype === "success" ? message.result : message.errors.join("\n");
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add null checks for optional message properties.

The SDKMessage type (from claude-agent-sdk.d.ts) has optional message and content fields. Accessing message.message.content on Line 229 will throw if message.message is undefined. Similarly, message.result and message.errors on Line 239 are optional.

🛡️ Proposed fix with defensive access
   if (message.type === "assistant") {
-    for (const block of message.message.content) {
+    const content = message.message?.content;
+    if (!content) return;
+    for (const block of content) {
       if (typeof block?.type === "string" && block.type.includes("thinking")) {
         summary.assistantThinkingBlockCount += 1;
       }
     }
     return;
   }

   if (message.type === "result") {
     summary.resultSubtype = message.subtype;
-    summary.resultText = message.subtype === "success" ? message.result : message.errors.join("\n");
+    summary.resultText = message.subtype === "success" 
+      ? (message.result ?? null)
+      : (message.errors?.join("\n") ?? null);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (message.type === "assistant") {
for (const block of message.message.content) {
if (typeof block?.type === "string" && block.type.includes("thinking")) {
summary.assistantThinkingBlockCount += 1;
}
}
return;
}
if (message.type === "result") {
summary.resultSubtype = message.subtype;
summary.resultText = message.subtype === "success" ? message.result : message.errors.join("\n");
}
}
if (message.type === "assistant") {
const content = message.message?.content;
if (!content) return;
for (const block of content) {
if (typeof block?.type === "string" && block.type.includes("thinking")) {
summary.assistantThinkingBlockCount += 1;
}
}
return;
}
if (message.type === "result") {
summary.resultSubtype = message.subtype;
summary.resultText = message.subtype === "success"
? (message.result ?? null)
: (message.errors?.join("\n") ?? null);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/claude-haiku-thinking-probe.ts` around lines 228 - 241, When handling
SDKMessage in the block that checks message.type === "assistant" and
message.type === "result", add defensive null/undefined checks before accessing
nested optional properties: ensure message.message exists and
message.message.content is an array before iterating and incrementing
summary.assistantThinkingBlockCount, and ensure message.result and
message.errors are present (fallback to empty string/array) before assigning
summary.resultText; also guard reading message.subtype into
summary.resultSubtype. Update the checks around the assistant loop and the
result branch (the code that references message.message.content,
message.subtype, message.result, and message.errors) so no property access
occurs on undefined.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
apps/web/src/appSettings.ts (1)

49-58: ⚠️ Potential issue | 🟠 Major

Migrate legacy claudeCode git overrides before normalization.

Once claudeCode is removed from PROVIDER_KINDS, persisted gitTextGenerationModelByProvider.claudeCode values are silently dropped the next time settings are parsed. Add a bridge in migratePersistedAppSettings that copies the legacy key to claudeAgent before normalization, without overwriting an existing claudeAgent value.

🛠️ Suggested migration patch
 function migratePersistedAppSettings(value: unknown): unknown {
   if (!value || typeof value !== "object" || Array.isArray(value)) {
     return value;
   }

   const settings = { ...(value as Record<string, unknown>) };
   if (settings.providerLogoAppearance === undefined && settings.grayscaleProviderLogos === true) {
     settings.providerLogoAppearance = "grayscale";
   }
+
+  const gitOverrides = settings.gitTextGenerationModelByProvider;
+  if (gitOverrides && typeof gitOverrides === "object" && !Array.isArray(gitOverrides)) {
+    const record = { ...(gitOverrides as Record<string, unknown>) };
+    if (typeof record.claudeAgent !== "string" && typeof record.claudeCode === "string") {
+      record.claudeAgent = record.claudeCode;
+    }
+    delete record.claudeCode;
+    settings.gitTextGenerationModelByProvider = record;
+  }

   return settings;
 }

Also applies to: 432-442

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/appSettings.ts` around lines 49 - 58, Add a migration bridge in
migratePersistedAppSettings to copy a legacy
gitTextGenerationModelByProvider.claudeCode value to
gitTextGenerationModelByProvider.claudeAgent before any normalization runs (so
the removal of "claudeCode" from PROVIDER_KINDS won't drop persisted values);
perform the copy only if claudeCode exists and claudeAgent is undefined/null to
avoid overwriting an existing claudeAgent, then remove the claudeCode key so
downstream normalization and PROVIDER_KINDS no longer need to handle it.
🧹 Nitpick comments (2)
apps/server/src/provider/Layers/ProviderHealth.ts (1)

282-301: Extract a generic CLI runner before this file drifts further.

This is now the third copy of the same spawn/collect/exit-code plumbing. A shared runCliCommand(binary, args) helper would make future provider additions and upstream syncs less error-prone.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ProviderHealth.ts` around lines 282 - 301,
Extract a reusable helper runCliCommand(binary: string, args:
ReadonlyArray<string>) that encapsulates the spawn/collect/exit-code plumbing
currently duplicated: use ChildProcessSpawner.ChildProcessSpawner to spawn a
ChildProcess.make(binary, [...args], { shell: process.platform === "win32" }),
collect stdout/stderr via collectStreamAsString, map child.exitCode to Number
and return { stdout, stderr, code } satisfying CommandResult; then replace the
existing runClaudeCommand implementation to call runCliCommand("claude", args)
so only runCliCommand holds the shared logic.
apps/server/src/provider/Layers/ClaudeAdapter.test.ts (1)

2532-2589: Exercise AskUserQuestion with a live turn.

This case invokes canUseTool immediately after startSession, so it bypasses the active-turn bookkeeping that runs in production. Starting a turn first would make the full-access assertion much more representative.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ClaudeAdapter.test.ts` around lines 2532 -
2589, Test calls canUseTool immediately after adapter.startSession which skips
the per-turn bookkeeping; start an active turn before invoking canUseTool so the
full-access assertion mirrors production. After creating the session via
adapter.startSession, invoke the adapter's turn-start method (e.g., call the
adapter method that begins a turn for the session — locate the method that
advances/starts a turn in this test harness) or otherwise trigger a live turn
via adapter.streamEvents (emit/await the turn-start event) before calling
createInput?.options.canUseTool; then continue using streamEvents,
respondToUserInput and permissionPromise as before so canUseTool runs within an
actual active turn.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/provider/Layers/ClaudeAdapter.test.ts`:
- Around line 1076-1101: The test currently forks a collector with
Effect.runFork over adapter.streamEvents, uses multiple yieldNow calls and
interruptUnsafe(), which causes races and unmanaged fibers; change it to scope
the collector and wait deterministically for the "session.exited" event: run the
stream with a scoped runner (e.g., Stream.runForEachScoped or Stream.runInto a
Queue) over adapter.streamEvents and push events into runtimeEvents inside that
scoped effect, start the session via adapter.startSession and sendTurn as
before, then await until runtimeEvents contains an event with type
"session.exited" (or pull from the queue until that event) before calling
harness.query.fail and making assertions; remove the yieldNow spins and
interruptUnsafe() so the collector is cleanly stopped by the scope when the test
completes.

In `@apps/server/src/provider/Layers/ProviderHealth.ts`:
- Around line 721-731: The conditional in ProviderHealth.ts that inspects
lowerOutput for auth errors currently checks for "run `claude login`" and "run
claude login" but not the CLI form you instruct users to run ("claude auth
login"), so add checks for "run `claude auth login`", "run claude auth login",
and a plain "claude auth login" to the same if block (the block that returns
status: "error" and authStatus: "unauthenticated") so emitted messages with the
"auth login" form are recognized as unauthenticated.
- Around line 62-66: The helper isCommandMissingCause currently checks for
"enoent" and "notfound" but omits the spaced form "command not found" which
other code paths (e.g., the missing-binary handler) expect; update
isCommandMissingCause to also check the lowercased error.message for the phrase
"command not found" (case-insensitive) in addition to the existing checks so the
function correctly identifies missing-command errors like "command not found".

---

Duplicate comments:
In `@apps/web/src/appSettings.ts`:
- Around line 49-58: Add a migration bridge in migratePersistedAppSettings to
copy a legacy gitTextGenerationModelByProvider.claudeCode value to
gitTextGenerationModelByProvider.claudeAgent before any normalization runs (so
the removal of "claudeCode" from PROVIDER_KINDS won't drop persisted values);
perform the copy only if claudeCode exists and claudeAgent is undefined/null to
avoid overwriting an existing claudeAgent, then remove the claudeCode key so
downstream normalization and PROVIDER_KINDS no longer need to handle it.

---

Nitpick comments:
In `@apps/server/src/provider/Layers/ClaudeAdapter.test.ts`:
- Around line 2532-2589: Test calls canUseTool immediately after
adapter.startSession which skips the per-turn bookkeeping; start an active turn
before invoking canUseTool so the full-access assertion mirrors production.
After creating the session via adapter.startSession, invoke the adapter's
turn-start method (e.g., call the adapter method that begins a turn for the
session — locate the method that advances/starts a turn in this test harness) or
otherwise trigger a live turn via adapter.streamEvents (emit/await the
turn-start event) before calling createInput?.options.canUseTool; then continue
using streamEvents, respondToUserInput and permissionPromise as before so
canUseTool runs within an actual active turn.

In `@apps/server/src/provider/Layers/ProviderHealth.ts`:
- Around line 282-301: Extract a reusable helper runCliCommand(binary: string,
args: ReadonlyArray<string>) that encapsulates the spawn/collect/exit-code
plumbing currently duplicated: use ChildProcessSpawner.ChildProcessSpawner to
spawn a ChildProcess.make(binary, [...args], { shell: process.platform ===
"win32" }), collect stdout/stderr via collectStreamAsString, map child.exitCode
to Number and return { stdout, stderr, code } satisfying CommandResult; then
replace the existing runClaudeCommand implementation to call
runCliCommand("claude", args) so only runCliCommand holds the shared logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 855c859c-70a4-4eeb-8990-b9ed1151e47c

📥 Commits

Reviewing files that changed from the base of the PR and between 487fd94 and 40839c1.

📒 Files selected for processing (5)
  • apps/server/src/provider/Layers/ClaudeAdapter.test.ts
  • apps/server/src/provider/Layers/ProviderHealth.ts
  • apps/web/src/appSettings.ts
  • packages/contracts/src/provider.test.ts
  • packages/contracts/src/provider.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/contracts/src/provider.test.ts

Comment on lines +1076 to +1101
const runtimeEventsFiber = Effect.runFork(
Stream.runForEach(adapter.streamEvents, (event) =>
Effect.sync(() => {
runtimeEvents.push(event);
}),
),
);

yield* adapter.startSession({
threadId: THREAD_ID,
provider: "claudeAgent",
runtimeMode: "full-access",
});

const turn = yield* adapter.sendTurn({
threadId: THREAD_ID,
input: "hello",
attachments: [],
});

harness.query.fail(new Error("All fibers interrupted without error"));

yield* Effect.yieldNow;
yield* Effect.yieldNow;
yield* Effect.yieldNow;
runtimeEventsFiber.interruptUnsafe();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n apps/server/src/provider/Layers/ClaudeAdapter.test.ts | sed -n '1050,1120p'

Repository: aaditagrawal/t3code

Length of output: 2941


🏁 Script executed:

# Check the full test method to understand the context
wc -l apps/server/src/provider/Layers/ClaudeAdapter.test.ts

Repository: aaditagrawal/t3code

Length of output: 121


🏁 Script executed:

# Find the test method that contains lines 1076-1101
ast-grep --pattern $'it($_, ($$$) => {
  $$$
  Effect.runFork($$$);
  $$$
  runtimeEventsFiber.interruptUnsafe();
  $$$
})'

Repository: aaditagrawal/t3code

Length of output: 45


🏁 Script executed:

# Search for other uses of Effect.runFork in test files to see patterns
rg 'Effect\.runFork' apps/server/src/provider/Layers/ -A 5 -B 2

Repository: aaditagrawal/t3code

Length of output: 12216


🏁 Script executed:

# Check if session.exited is used as an event in the codebase
rg 'session\.exited|sessionExited' apps/server/src/provider/Layers/ClaudeAdapter.test.ts

Repository: aaditagrawal/t3code

Length of output: 195


Avoid scheduler-dependent stream collection in this abort test.

Effect.runFork plus three yieldNows makes the assertion race the consumer, and interruptUnsafe() can leave the collector unmanaged if the test exits early. Keep the collector scoped to the test and wait for a deterministic stop condition—e.g. the "session.exited" event appearing in runtimeEvents—before asserting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ClaudeAdapter.test.ts` around lines 1076 -
1101, The test currently forks a collector with Effect.runFork over
adapter.streamEvents, uses multiple yieldNow calls and interruptUnsafe(), which
causes races and unmanaged fibers; change it to scope the collector and wait
deterministically for the "session.exited" event: run the stream with a scoped
runner (e.g., Stream.runForEachScoped or Stream.runInto a Queue) over
adapter.streamEvents and push events into runtimeEvents inside that scoped
effect, start the session via adapter.startSession and sendTurn as before, then
await until runtimeEvents contains an event with type "session.exited" (or pull
from the queue until that event) before calling harness.query.fail and making
assertions; remove the yieldNow spins and interruptUnsafe() so the collector is
cleanly stopped by the scope when the test completes.

Comment on lines 62 to 66
function isCommandMissingCause(error: unknown): boolean {
if (!(error instanceof Error)) return false;
const lower = error.message.toLowerCase();
return (
lower.includes("command not found: codex") ||
lower.includes("spawn codex enoent") ||
lower.includes("enoent") ||
lower.includes("notfound")
);
return lower.includes("enoent") || lower.includes("notfound");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Restore the spaced missing-command match in the shared helper.

Line 65 drops the spaced "command not found" form that Line 468 still treats as a missing-binary signal. That regresses Codex/Claude from the clearer “not installed or not on PATH” message to a generic execution failure for the same outcome.

🩹 Proposed fix
 function isCommandMissingCause(error: unknown): boolean {
   if (!(error instanceof Error)) return false;
   const lower = error.message.toLowerCase();
-  return lower.includes("enoent") || lower.includes("notfound");
+  return (
+    lower.includes("enoent") ||
+    lower.includes("notfound") ||
+    lower.includes("command not found")
+  );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function isCommandMissingCause(error: unknown): boolean {
if (!(error instanceof Error)) return false;
const lower = error.message.toLowerCase();
return (
lower.includes("command not found: codex") ||
lower.includes("spawn codex enoent") ||
lower.includes("enoent") ||
lower.includes("notfound")
);
return lower.includes("enoent") || lower.includes("notfound");
}
function isCommandMissingCause(error: unknown): boolean {
if (!(error instanceof Error)) return false;
const lower = error.message.toLowerCase();
return (
lower.includes("enoent") ||
lower.includes("notfound") ||
lower.includes("command not found")
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ProviderHealth.ts` around lines 62 - 66, The
helper isCommandMissingCause currently checks for "enoent" and "notfound" but
omits the spaced form "command not found" which other code paths (e.g., the
missing-binary handler) expect; update isCommandMissingCause to also check the
lowercased error.message for the phrase "command not found" (case-insensitive)
in addition to the existing checks so the function correctly identifies
missing-command errors like "command not found".

Comment on lines +721 to +731
if (
lowerOutput.includes("not logged in") ||
lowerOutput.includes("login required") ||
lowerOutput.includes("authentication required") ||
lowerOutput.includes("run `claude login`") ||
lowerOutput.includes("run claude login")
) {
return {
status: "error",
authStatus: "unauthenticated",
message: "Claude is not authenticated. Run `claude auth login` and try again.",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Match the recovery command you already tell users to run.

Line 731 points users to claude auth login, but Lines 725-726 only recognize claude login. If the CLI emits the auth login form, this falls through to warning/unknown instead of error/unauthenticated.

🩹 Proposed fix
   if (
     lowerOutput.includes("not logged in") ||
     lowerOutput.includes("login required") ||
     lowerOutput.includes("authentication required") ||
+    lowerOutput.includes("run `claude auth login`") ||
+    lowerOutput.includes("run claude auth login") ||
     lowerOutput.includes("run `claude login`") ||
     lowerOutput.includes("run claude login")
   ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ProviderHealth.ts` around lines 721 - 731,
The conditional in ProviderHealth.ts that inspects lowerOutput for auth errors
currently checks for "run `claude login`" and "run claude login" but not the CLI
form you instruct users to run ("claude auth login"), so add checks for "run
`claude auth login`", "run claude auth login", and a plain "claude auth login"
to the same if block (the block that returns status: "error" and authStatus:
"unauthenticated") so emitted messages with the "auth login" form are recognized
as unauthenticated.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/appSettings.ts (1)

394-429: ⚠️ Potential issue | 🟠 Major

Unify legacy-key migration across both settings read paths to prevent data loss.

migratePersistedAppSettings() runs only in parsePersistedSettings(), which is used by getAppSettingsSnapshot(). However, useAppSettings() reads directly through useLocalStorage(..., AppSettingsSchema), which bypasses the migration. Since useLocalStorage's decode function only applies schema validation without any pre-processing, legacy gitTextGenerationModelByProvider.claudeCode values will be invisible to React state and silently dropped on the next normalized write. This creates two inconsistent behaviors for the same persisted data.

Move the migration logic into useLocalStorage's decode path or a shared parser function so both entry points apply it consistently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/appSettings.ts` around lines 394 - 429, The migration for legacy
keys (e.g., gitTextGenerationModelByProvider.claudeCode) currently runs only in
parsePersistedSettings, causing useAppSettings (which uses useLocalStorage with
AppSettingsSchema) to skip migration and drop legacy values; update the code so
both paths run the same migration: either call migratePersistedAppSettings (or
refactor parsePersistedSettings into a shared parse function) inside
useLocalStorage’s decode logic, or have useAppSettings call the same parser used
by getAppSettingsSnapshot; ensure the migration of
gitTextGenerationModelByProvider and providerLogoAppearance happens before
AppSettingsSchema.makeUnsafe/validation so normalized writes preserve migrated
keys.
♻️ Duplicate comments (4)
packages/shared/src/model.ts (2)

466-481: ⚠️ Potential issue | 🟠 Major

inferProviderForModel() still only checks claudeAgent and codex providers.

The function currently only tests Claude Agent and Codex catalogs. Provider-specific slugs like composer-1.5 (cursor), smart (amp), and gemini-2.5-pro (geminiCli) will incorrectly fall back to the default instead of resolving to their respective providers.

Consider iterating over all MODEL_SLUG_SET_BY_PROVIDER keys to support the full provider registry.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/shared/src/model.ts` around lines 466 - 481, inferProviderForModel
currently only checks "claudeAgent" and "codex"; update it to iterate over all
providers in MODEL_SLUG_SET_BY_PROVIDER instead of hardcoding two providers. For
each provider key, call normalizeModelSlug(model, providerKey) and if the
normalized slug exists in MODEL_SLUG_SET_BY_PROVIDER[providerKey], return that
provider (cast/typed as ProviderKind). Preserve the existing fallback behavior
(including the special "claude-" prefix check) if no provider matches. Use the
existing symbols inferProviderForModel, normalizeModelSlug,
MODEL_SLUG_SET_BY_PROVIDER, and ProviderKind to locate and implement this loop.

610-625: ⚠️ Potential issue | 🟡 Minor

Make the ultrathink prefix guard case-insensitive.

Line 621 checks trimmed.startsWith("Ultrathink:") with exact casing. If a user writes "ultrathink:" or "ULTRATHINK:", a second prefix will be prepended.

💡 Proposed fix
-  if (trimmed.startsWith("Ultrathink:")) {
+  if (/^ultrathink:/i.test(trimmed)) {
     return trimmed;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/shared/src/model.ts` around lines 610 - 625, The guard in
applyClaudePromptEffortPrefix uses trimmed.startsWith("Ultrathink:") which is
case-sensitive; change it to a case-insensitive check (e.g., use
trimmed.toLowerCase().startsWith("ultrathink:") or a case-insensitive regex like
/^ultrathink:/i) so values like "ultrathink:" or "ULTRATHINK:" are recognized
and the prefix is not duplicated; keep the rest of the function behavior the
same and reference the trimmed variable and applyClaudePromptEffortPrefix
function when making the change.
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts (1)

167-168: ⚠️ Potential issue | 🟡 Minor

Clear threadModelOptions when a thread is torn down.

The threadModelOptions map is populated on line 399 but never cleared. Unlike threadProviderOptions, it is not deleted in thread.deleted (line 755) or thread.session-stop-requested (line 729) handlers. This causes a memory leak for long-lived servers.

💡 Proposed fix for thread.deleted handler
         yield* providerService
           .stopSession({ threadId })
           .pipe(Effect.catchCause(() => Effect.void));
         threadProviderOptions.delete(threadId);
+        threadModelOptions.delete(threadId);
         return;
💡 Proposed fix for session-stop-requested handler
     threadProviderOptions.delete(thread.id);
+    threadModelOptions.delete(thread.id);

     yield* setThreadSession({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/orchestration/Layers/ProviderCommandReactor.ts` around lines
167 - 168, threadModelOptions (the Map instantiated as threadModelOptions) is
never cleaned up, causing a memory leak; update the same teardown points that
remove threadProviderOptions to also remove entries from threadModelOptions —
specifically, in the handlers for "thread.deleted" and
"thread.session-stop-requested" (the functions where
threadProviderOptions.delete(threadId) is called) add logic to delete the same
threadId from threadModelOptions (or clear any nested state) so entries are
removed when a thread is torn down.
apps/web/src/composerDraftStore.ts (1)

792-804: ⚠️ Potential issue | 🟠 Major

Flush debounced draft storage before probing persisted attachment IDs.

The probe on Line 1558 can run before the debounced write from Line 34-Line 37 is committed, so this read path may observe stale storage and incorrectly classify newly staged attachments as non-persisted.

💡 Proposed fix
 function readPersistedAttachmentIdsFromStorage(threadId: ThreadId): string[] {
   if (threadId.length === 0) {
     return [];
   }
   try {
+    composerDebouncedStorage.flush();
     const persisted = getLocalStorageItem(
       COMPOSER_DRAFT_STORAGE_KEY,
       PersistedComposerDraftStoreStorage,
     );

Also applies to: 1558-1560

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/composerDraftStore.ts` around lines 792 - 804, The
readPersistedAttachmentIdsFromStorage function can observe stale LocalStorage
because a debounced draft write may not have been flushed; before reading
COMPOSER_DRAFT_STORAGE_KEY and validating
PersistedComposerDraftStoreStorage/COMPOSER_DRAFT_STORAGE_VERSION, ensure you
synchronously flush any pending debounced draft writes by calling the
debounce-flush helper (e.g. flushDebouncedDraftStorage or the function that
commits the debounced drafts) and await it if it returns a promise, then proceed
with the current logic to return
persisted.state.draftsByThreadId[threadId]?.attachments.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/web/src/appSettings.ts`:
- Around line 394-429: The migration for legacy keys (e.g.,
gitTextGenerationModelByProvider.claudeCode) currently runs only in
parsePersistedSettings, causing useAppSettings (which uses useLocalStorage with
AppSettingsSchema) to skip migration and drop legacy values; update the code so
both paths run the same migration: either call migratePersistedAppSettings (or
refactor parsePersistedSettings into a shared parse function) inside
useLocalStorage’s decode logic, or have useAppSettings call the same parser used
by getAppSettingsSnapshot; ensure the migration of
gitTextGenerationModelByProvider and providerLogoAppearance happens before
AppSettingsSchema.makeUnsafe/validation so normalized writes preserve migrated
keys.

---

Duplicate comments:
In `@apps/server/src/orchestration/Layers/ProviderCommandReactor.ts`:
- Around line 167-168: threadModelOptions (the Map instantiated as
threadModelOptions) is never cleaned up, causing a memory leak; update the same
teardown points that remove threadProviderOptions to also remove entries from
threadModelOptions — specifically, in the handlers for "thread.deleted" and
"thread.session-stop-requested" (the functions where
threadProviderOptions.delete(threadId) is called) add logic to delete the same
threadId from threadModelOptions (or clear any nested state) so entries are
removed when a thread is torn down.

In `@apps/web/src/composerDraftStore.ts`:
- Around line 792-804: The readPersistedAttachmentIdsFromStorage function can
observe stale LocalStorage because a debounced draft write may not have been
flushed; before reading COMPOSER_DRAFT_STORAGE_KEY and validating
PersistedComposerDraftStoreStorage/COMPOSER_DRAFT_STORAGE_VERSION, ensure you
synchronously flush any pending debounced draft writes by calling the
debounce-flush helper (e.g. flushDebouncedDraftStorage or the function that
commits the debounced drafts) and await it if it returns a promise, then proceed
with the current logic to return
persisted.state.draftsByThreadId[threadId]?.attachments.

In `@packages/shared/src/model.ts`:
- Around line 466-481: inferProviderForModel currently only checks "claudeAgent"
and "codex"; update it to iterate over all providers in
MODEL_SLUG_SET_BY_PROVIDER instead of hardcoding two providers. For each
provider key, call normalizeModelSlug(model, providerKey) and if the normalized
slug exists in MODEL_SLUG_SET_BY_PROVIDER[providerKey], return that provider
(cast/typed as ProviderKind). Preserve the existing fallback behavior (including
the special "claude-" prefix check) if no provider matches. Use the existing
symbols inferProviderForModel, normalizeModelSlug, MODEL_SLUG_SET_BY_PROVIDER,
and ProviderKind to locate and implement this loop.
- Around line 610-625: The guard in applyClaudePromptEffortPrefix uses
trimmed.startsWith("Ultrathink:") which is case-sensitive; change it to a
case-insensitive check (e.g., use
trimmed.toLowerCase().startsWith("ultrathink:") or a case-insensitive regex like
/^ultrathink:/i) so values like "ultrathink:" or "ULTRATHINK:" are recognized
and the prefix is not duplicated; keep the rest of the function behavior the
same and reference the trimmed variable and applyClaudePromptEffortPrefix
function when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4229a73f-2f89-4949-b35e-5cbcad7667d7

📥 Commits

Reviewing files that changed from the base of the PR and between 40839c1 and 76c672a.

📒 Files selected for processing (6)
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
  • apps/server/src/provider/Layers/ProviderService.test.ts
  • apps/web/src/appSettings.ts
  • apps/web/src/composerDraftStore.ts
  • packages/contracts/src/providerRuntime.ts
  • packages/shared/src/model.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/contracts/src/providerRuntime.ts
  • apps/server/src/provider/Layers/ProviderService.test.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/web/src/appSettings.ts (1)

457-470: Normalize the hook read path too.

getAppSettingsSnapshot() returns normalized settings, but useAppSettings() still returns only the renamed-key variant. Existing localStorage entries can therefore behave differently depending on which read path is used until a write happens.

♻️ Suggested patch
   const migratedSettings = useMemo(() => {
     let next = settings;
     ...
-    return next;
+    return normalizeAppSettings(next);
   }, [settings]);

Also applies to: 484-484

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/appSettings.ts` around lines 457 - 470, The useAppSettings()
hook currently returns the un-migrated settings while getAppSettingsSnapshot()
returns normalized settings; update the hook to return the migratedSettings (the
result of the legacy key migration logic) so both read paths are consistent.
Locate the migration logic that produces migratedSettings (checks
settings.gitTextGenerationModelByProvider for "claudeCode" and maps it to
"claudeAgent") and ensure useAppSettings() and any other read paths (referenced
by the symbol useAppSettings) use that migratedSettings instead of the raw
settings object so localStorage entries with old keys are normalized on read.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/appSettings.ts`:
- Around line 404-415: The migration only renames claudeCode inside
gitTextGenerationModelByProvider; also migrate providerAccentColors: when
settings.providerAccentColors is a plain object (not array) and contains
"claudeCode" but not "claudeAgent", move the value to "claudeAgent" and delete
"claudeCode". Apply the same logic in both places where the code handles legacy
keys (the existing gitTextGenerationModelByProvider migration block and the
similar block around lines 457-470), using the same type checks and preserving
other keys when building the new object.

---

Nitpick comments:
In `@apps/web/src/appSettings.ts`:
- Around line 457-470: The useAppSettings() hook currently returns the
un-migrated settings while getAppSettingsSnapshot() returns normalized settings;
update the hook to return the migratedSettings (the result of the legacy key
migration logic) so both read paths are consistent. Locate the migration logic
that produces migratedSettings (checks settings.gitTextGenerationModelByProvider
for "claudeCode" and maps it to "claudeAgent") and ensure useAppSettings() and
any other read paths (referenced by the symbol useAppSettings) use that
migratedSettings instead of the raw settings object so localStorage entries with
old keys are normalized on read.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 35bcf9be-ff0b-4321-bf11-2b393a726d91

📥 Commits

Reviewing files that changed from the base of the PR and between cf638c6 and 667b454.

📒 Files selected for processing (1)
  • apps/web/src/appSettings.ts

@aaditagrawal aaditagrawal merged commit beb3f63 into main Mar 20, 2026
8 checks passed
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.

4 participants