diff --git a/src/main/agent-fix/claude-spawner.ts b/src/main/agent-fix/claude-spawner.ts index 75c84626..c2ae9064 100644 --- a/src/main/agent-fix/claude-spawner.ts +++ b/src/main/agent-fix/claude-spawner.ts @@ -1,9 +1,43 @@ import { spawn } from 'child_process' -import type { FixProgressEvent } from '../../shared/types' +import type { FixConfig, FixProgressEvent } from '../../shared/types' import { truncate } from '../../shared/annotation-utils' import { getFixConfig } from '../runtime/preferences' import { parseStreamLine } from './stream-json-parser' +const DEFAULT_LOCAL_BASE_URL = 'http://localhost:1234' +const DEFAULT_LOCAL_AUTH_TOKEN = 'lmstudio' + +export function buildClaudeInvocation( + prompt: string, + config: FixConfig, +): { args: string[]; env: Record } { + const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose'] + const env: Record = { NO_COLOR: '1' } + + if (config.model === 'local') { + // Point Claude Code's CLI at an Anthropic-compatible local server + // (LM Studio ≥ 0.4.1 exposes /v1/messages). The CLI still handles + // skills, permissions, and stream-json on the client side. + const baseUrl = config.baseUrl?.trim() || DEFAULT_LOCAL_BASE_URL + const authToken = config.authToken?.trim() || DEFAULT_LOCAL_AUTH_TOKEN + env.ANTHROPIC_BASE_URL = baseUrl + env.ANTHROPIC_AUTH_TOKEN = authToken + const modelId = config.modelId?.trim() + if (modelId) { + env.ANTHROPIC_MODEL = modelId + args.push('--model', modelId) + } + } else if (config.model !== 'opus') { + args.push('--model', `claude-${config.model}-4-6`) + } + + if (config.permissions === 'dangerously') { + args.push('--dangerously-skip-permissions') + } + + return { args, env } +} + export interface FixResult { summary: string shouldResolve: boolean @@ -38,25 +72,14 @@ export function invokeClaude( const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS return new Promise((resolve, reject) => { - const config = getFixConfig() - const args = [ - '-p', prompt, - '--output-format', 'stream-json', - '--verbose', - ] - if (config.model !== 'opus') { - args.push('--model', `claude-${config.model}-4-6`) - } - if (config.permissions === 'dangerously') { - args.push('--dangerously-skip-permissions') - } + const { args, env } = buildClaudeInvocation(prompt, getFixConfig()) const child = spawn( 'claude', args, { stdio: ['pipe', 'pipe', 'pipe'], cwd: repoPath, - env: { ...process.env, NO_COLOR: '1' }, + env: { ...process.env, ...env }, }, ) diff --git a/src/main/ipc/register-right-details-panel-ipc.ts b/src/main/ipc/register-right-details-panel-ipc.ts index 32768a66..6c61164e 100644 --- a/src/main/ipc/register-right-details-panel-ipc.ts +++ b/src/main/ipc/register-right-details-panel-ipc.ts @@ -1,5 +1,5 @@ import { BrowserWindow, dialog, ipcMain } from 'electron' -import type { AnnotationCreateRequest, EdgeEnd, EdgeSide } from '../../shared/types' +import type { AnnotationCreateRequest, EdgeEnd, EdgeSide, FixConfigPatch } from '../../shared/types' import { getOriginBinding, removeOriginBinding, @@ -242,9 +242,9 @@ export function registerRightDetailsPanelIpc(): void { ipcMain.on( 'right-details-panel-set-fix-config', - (_event, payload: { model?: string; permissions?: string } | undefined) => { + (_event, payload: FixConfigPatch | undefined) => { if (!payload) return - setFixConfig(payload as { model?: 'opus' | 'sonnet' | 'haiku'; permissions?: 'dangerously' | 'default' }) + setFixConfig(payload) notifyDevtoolsPanelData() }, ) diff --git a/src/main/runtime/preferences.ts b/src/main/runtime/preferences.ts index a67015a4..e465a81b 100644 --- a/src/main/runtime/preferences.ts +++ b/src/main/runtime/preferences.ts @@ -9,6 +9,7 @@ import type { CursorMotionParams, DevtoolsPanelTab, FixConfig, + FixConfigPatch, OnboardingState, OriginBinding, OriginBindings, @@ -199,7 +200,13 @@ export function savePreferences(): void { devtoolsWidth: uiDevtoolsWidth(), devtoolsPanelTab: uiDevtoolsPanelTab(), originBindings, - fixConfig: { model: fixConfig.model, permissions: fixConfig.permissions }, + fixConfig: { + model: fixConfig.model, + permissions: fixConfig.permissions, + baseUrl: fixConfig.baseUrl, + modelId: fixConfig.modelId, + authToken: fixConfig.authToken, + }, }) } @@ -228,7 +235,7 @@ export function getFixConfig(): FixConfig { return fixConfig } -export function setFixConfig(patch: { model?: FixConfig['model']; permissions?: FixConfig['permissions'] }): void { +export function setFixConfig(patch: FixConfigPatch): void { fixConfig = { ...fixConfig, ...patch, configured: true } savePreferences() } diff --git a/src/preload/right-details-panel.ts b/src/preload/right-details-panel.ts index 51ab74bc..85d9fd8d 100644 --- a/src/preload/right-details-panel.ts +++ b/src/preload/right-details-panel.ts @@ -3,6 +3,7 @@ import type { AnnotationCreateRequest, DevtoolsPanelData, DevtoolsPanelElectronAPI, + FixConfigPatch, ThemeData, } from '../shared/types' @@ -39,7 +40,7 @@ const api: DevtoolsPanelElectronAPI = { ipcRenderer.send('right-details-panel-pick-repo-for-origin', { origin }), removeOriginBinding: (origin: string) => ipcRenderer.send('right-details-panel-remove-origin-binding', { origin }), - setFixConfig: (config: { model: string; permissions: string }) => + setFixConfig: (config: FixConfigPatch) => ipcRenderer.send('right-details-panel-set-fix-config', config), updateTextEntity: (id: string, patch: { color?: string }) => ipcRenderer.send('canvas-update-text-entity', { id, patch }), diff --git a/src/renderer/right-details-panel/components/DocumentPane.tsx b/src/renderer/right-details-panel/components/DocumentPane.tsx index 7ce28723..537dc4a9 100644 --- a/src/renderer/right-details-panel/components/DocumentPane.tsx +++ b/src/renderer/right-details-panel/components/DocumentPane.tsx @@ -229,6 +229,9 @@ function FixSettingsView({ }) { const [model, setModel] = useState(fixConfig.model) const [permissions, setPermissions] = useState(fixConfig.permissions) + const [baseUrl, setBaseUrl] = useState(fixConfig.baseUrl ?? '') + const [modelId, setModelId] = useState(fixConfig.modelId ?? '') + const [authToken, setAuthToken] = useState(fixConfig.authToken ?? '') const selectClass = `w-full rounded-md border px-2 py-1.5 text-[12px] ${ isDark @@ -237,7 +240,11 @@ function FixSettingsView({ }` const handleConfirm = () => { - rightDetailsPanelApi.setFixConfig({ model, permissions }) + rightDetailsPanelApi.setFixConfig( + model === 'local' + ? { model, permissions, baseUrl, modelId, authToken } + : { model, permissions }, + ) onDone() } @@ -259,9 +266,49 @@ function FixSettingsView({ + + {model === 'local' ? ( +
+
+ + setBaseUrl(e.target.value)} + placeholder="http://localhost:1234" + className={selectClass} + /> +
+
+ + setModelId(e.target.value)} + placeholder="qwen/qwen3-coder-30b" + className={selectClass} + /> +
+
+ + setAuthToken(e.target.value)} + placeholder="lmstudio" + className={selectClass} + /> +
+

+ Claude Code will POST to {'{baseUrl}'}/v1/messages. Requires LM Studio 0.4.1+ with a + loaded model and the server started. +

+
+ ) : null} +