Skip to content

fix: use system claude binary in packaged Electron app#1188

Closed
JustYannicc wants to merge 10 commits intopingdotgg:codething/648ca884-claudefrom
JustYannicc:fix/claude-adapter-system-binary
Closed

fix: use system claude binary in packaged Electron app#1188
JustYannicc wants to merge 10 commits intopingdotgg:codething/648ca884-claudefrom
JustYannicc:fix/claude-adapter-system-binary

Conversation

@JustYannicc
Copy link

@JustYannicc JustYannicc commented Mar 18, 2026

Summary

The claude-agent-sdk bundles its own cli.js inside node_modules. In the packaged Electron app this file lives inside the asar archive, where child_process.spawn cannot execute it — causing the Claude Code process to exit with code 1 immediately after session start.

This defaults pathToClaudeCodeExecutable to "claude" (resolved via PATH) so the adapter uses the system-installed binary, matching how the Codex adapter already defaults to "codex".

Stack

Validation

  • Verified the SDK query() works correctly when pointed at the system claude binary
  • Confirmed the packaged DMG build launches and the claudeAgent provider no longer crashes on session start
  • bun typecheck

Note

Fix Claude binary path to use system claude executable in packaged Electron app

  • Defaults pathToClaudeCodeExecutable to 'claude' (system PATH) when no binaryPath is provided in provider options, fixing resolution in packaged Electron builds.
  • Adds effort-level support for Claude Agent: introduces ClaudeCodeEffort and ProviderReasoningEffort types, maps 'ultrathink' to 'max' at runtime, and prefixes prompts with 'Ultrathink:' when that effort is selected.
  • Propagates modelOptions (including effort) through session start, turn dispatch, session persistence, and stale-session recovery.
  • Adds synthetic turn auto-start when an assistant message arrives with no active turn, and auto-completes stale synthetic turns on sendTurn instead of returning a validation error.
  • UI updates include an animated Ultrathink pill/frame in the composer, a provider-agnostic ProviderTraitsPicker, and effort options surfaced for Claude in the compact controls menu.
📊 Macroscope summarized 87cad84. 21 files reviewed, 7 issues evaluated, 2 issues filtered, 3 comments posted

🗂️ Filtered Issues

apps/server/src/provider/Layers/ClaudeAdapter.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 2205: The change at line 2205 always sets pathToClaudeCodeExecutable to "claude" as a fallback when providerOptions?.binaryPath is undefined. Previously, when binaryPath wasn't provided, the option was omitted entirely, allowing the SDK to use its built-in default (a cli.js relative to the SDK package). Now, setting it to "claude" assumes the binary is available in the system PATH, which will cause ENOENT failures in environments where Claude Code isn't installed globally but the SDK's bundled CLI would have been used. The SDK's fallback logic in query() only triggers when pathToClaudeCodeExecutable is falsy—providing "claude" bypasses this entirely. [ Out of scope (triage) ]
apps/server/src/provider/Layers/ProviderService.ts — 1 comment posted, 2 evaluated, 1 filtered
  • line 210: In the "adopt-existing" session recovery path (line 210), upsertSessionBinding is called without preserving modelOptions and providerOptions from input.binding.runtimePayload. When an existing active session is adopted, the binding is overwritten without these persisted options, losing them for any future recovery attempts. [ Out of scope ]

JustYannicc and others added 10 commits March 17, 2026 10:41
- Auto-start synthetic turns for assistant messages arriving without an
  active turnState (fixes invisible background agent/subagent responses
  that arrive between user-initiated turns)
- Auto-close stale synthetic turns in sendTurn to prevent session deadlock
  when a background response left an orphaned turn open
- Clear inFlightTools between turns in completeTurn to prevent stale JSON
  fragments from corrupting the next turn's tool inputs
- Fix missing Fiber import in ProviderHealth

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The clear() was placed before the loop that emits item.completed events
for remaining in-flight tools, causing the loop to never execute. Move
it after the loop so tools get properly finalized before cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Updates all remaining references from "claudeCode" to "claudeAgent"
to match the Anthropic branding guidelines applied upstream. Includes
provider kind literals, object property keys, test payloads, and
component comparisons across server, web, and shared packages.

Also fixes synthetic turn state to match upstream's refactored
ClaudeTurnState interface (assistantTextBlocks map instead of the
old assistantItemId/messageCompleted/emittedTextDelta fields).

Also fixes inFlightTools.clear() placement: moved after the loop
that emits item.completed events for remaining in-flight tools,
so tools are properly finalized before cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The claude-agent-sdk bundles its own cli.js inside node_modules. In the
packaged Electron app this file lives inside the asar archive, where
child_process.spawn cannot execute it — causing the Claude Code process
to exit with code 1 immediately after session start.

Default pathToClaudeCodeExecutable to "claude" (resolved via PATH) so
the adapter uses the system-installed binary, matching how the Codex
adapter already defaults to "codex".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

Important

Review skipped

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

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c7bedb07-3241-47a0-b52d-0e74ad7faad3

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions bot added size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Mar 18, 2026
@JustYannicc JustYannicc marked this pull request as draft March 18, 2026 09:38
});
});

it("forwards claude effort options through session start and turn send", async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

🟢 Low Layers/ProviderCommandReactor.test.ts:354

The test "forwards claude effort options through session start and turn send" (lines 354-402) dispatches provider: "claudeAgent" with model: "claude-sonnet-4-6" on thread-1, but that thread was created with model: "gpt-5-codex" which infers provider codex. The ensureSessionForThread logic rejects provider switches with a ProviderAdapterRequestError, so startSession is never called and waitFor(() => harness.startSession.mock.calls.length === 1) will time out. The test appears to expect success, but the existing test at line 442 confirms this exact rejection behavior for provider mismatches. Consider adjusting the test to use a thread with a matching Claude model, or update assertions to expect the rejection.

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

The test "forwards claude effort options through session start and turn send" (lines 354-402) dispatches `provider: "claudeAgent"` with `model: "claude-sonnet-4-6"` on thread-1, but that thread was created with `model: "gpt-5-codex"` which infers provider `codex`. The `ensureSessionForThread` logic rejects provider switches with a `ProviderAdapterRequestError`, so `startSession` is never called and `waitFor(() => harness.startSession.mock.calls.length === 1)` will time out. The test appears to expect success, but the existing test at line 442 confirms this exact rejection behavior for provider mismatches. Consider adjusting the test to use a thread with a matching Claude model, or update assertions to expect the rejection.

Evidence trail:
1. apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts:234 - `createHarness()` creates thread-1 with `model: "gpt-5-codex"`
2. apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts:354-402 - Test dispatches turn with `provider: "claudeAgent"`, `model: "claude-sonnet-4-6"` and expects `startSession` to be called
3. packages/shared/src/model.ts:70-85 - `inferProviderForModel` function that determines provider from model
4. packages/shared/src/model.test.ts:107 - Confirms `inferProviderForModel("gpt-5.3-codex")` returns `"codex"`
5. apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:220-226 - Provider mismatch check that returns `ProviderAdapterRequestError`
6. apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts:442-489 - Existing test confirming provider mismatch rejection behavior

Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Medium

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

In recoverSessionForThread, the recovered session's modelOptions and providerOptions are passed to adapter.startSession (lines 235–236) but not to upsertSessionBinding at line 247. This causes the binding stored in directory to lose those options, so any subsequent recovery attempts will fail to restore them. Consider passing persistedModelOptions and persistedProviderOptions to upsertSessionBinding to preserve the options across recoveries.

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

In `recoverSessionForThread`, the recovered session's `modelOptions` and `providerOptions` are passed to `adapter.startSession` (lines 235–236) but not to `upsertSessionBinding` at line 247. This causes the binding stored in `directory` to lose those options, so any subsequent recovery attempts will fail to restore them. Consider passing `persistedModelOptions` and `persistedProviderOptions` to `upsertSessionBinding` to preserve the options across recoveries.

Evidence trail:
apps/server/src/provider/Layers/ProviderService.ts lines 162-174 (upsertSessionBinding definition with optional extra param), lines 89-101 (toRuntimePayloadFromSession only includes modelOptions/providerOptions if extra param is provided), lines 228-229 (persistedModelOptions and persistedProviderOptions are read), lines 235-236 (options passed to adapter.startSession), line 247 (upsertSessionBinding called WITHOUT the options), lines 308-311 (correct usage example with extra param in startSession)

Comment on lines +137 to +140
props.provider === "claudeAgent" ? "text-[#d97757]" : "text-muted-foreground/70",
props.provider === "claudeAgent" && props.ultrathinkActive
? "ultrathink-chroma"
: undefined,
Copy link
Contributor

Choose a reason for hiding this comment

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

🟢 Low chat/ProviderModelPicker.tsx:137

When lockedProvider differs from provider, the provider icon is determined by activeProvider (which uses lockedProvider when set) but the styling conditions on lines 137-139 check props.provider. This causes the Claude icon to render without the expected text-[#d97757] color and ultrathink-chroma animation when lockedProvider is claudeAgent but provider is something else. Change the conditions to use activeProvider to match the icon selection logic.

-              props.provider === "claudeAgent" ? "text-[#d97757]" : "text-muted-foreground/70",
-              props.provider === "claudeAgent" && props.ultrathinkActive
+              activeProvider === "claudeAgent" ? "text-[#d97757]" : "text-muted-foreground/70",
+              activeProvider === "claudeAgent" && props.ultrathinkActive
                 ? "ultrathink-chroma"
                 : undefined,
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/components/chat/ProviderModelPicker.tsx around lines 137-140:

When `lockedProvider` differs from `provider`, the provider icon is determined by `activeProvider` (which uses `lockedProvider` when set) but the styling conditions on lines 137-139 check `props.provider`. This causes the Claude icon to render without the expected `text-[#d97757]` color and `ultrathink-chroma` animation when `lockedProvider` is claudeAgent but `provider` is something else. Change the conditions to use `activeProvider` to match the icon selection logic.

Evidence trail:
apps/web/src/components/chat/ProviderModelPicker.tsx:
- Line 87: `const activeProvider = props.lockedProvider ?? props.provider;`
- Line 91: `const ProviderIcon = PROVIDER_ICON_BY_PROVIDER[activeProvider];`
- Lines 137-140: Styling conditions use `props.provider` instead of `activeProvider`

Commit: REVIEWED_COMMIT

@JustYannicc JustYannicc deleted the fix/claude-adapter-system-binary branch March 18, 2026 09:50
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:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant