mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-30 20:39:46 +02:00
Code mode: make packaged builds work via managed engine provisioning (#625)
* fix(code-mode): make packaged code mode work via on-demand engine provisioning Packaged builds could never run code mode: the Claude/Codex ACP adapters are spawned as separate `node <entry>` processes resolved at runtime, but esbuild can't inline a dynamic spawn target and Forge strips the workspace node_modules, so every release threw `Cannot find module '@agentclientprotocol/...'`. Dev worked only because of the pnpm symlink. Rather than bundle the ~400 MB of native engines (one claude + one codex binary per OS), provision them on demand: - forge.config.cjs: stage the two ACP adapters + their JS dependency closure into .package/acp/node_modules (npm-style nested layout, native engines skipped), exempt .package from the node_modules ignore rule, and only sign/notarize when APPLE_ID is set so unsigned local/CI builds can package. - agents.ts: resolve the adapter from the staged location first (node_modules fallback in dev); provision the pinned engine and point the adapter at it via CLAUDE_CODE_EXECUTABLE / CODEX_PATH. No dependence on a user's global install. - engine-provisioner.ts: ensureEngine() downloads the per-platform engine package from npm AT THE EXACT VERSION THE ADAPTER WAS BUILT AGAINST, verifies its sha512 integrity, extracts atomically into ~/.rowboat/engines/<agent>/<version>/, and caches it. Version-pinning keeps the ACP handshake compatible. - engine-manifest.ts + scripts/gen-engine-manifest.mjs: committed manifest of tarball URLs + integrity for all platforms, regenerated from the adapters' pinned versions on a bump. Verified on macOS arm64: both engines provision and run, and both adapters complete the ACP initialize handshake from the packaged .app against the provisioned engines. Installer drops from ~790 MB to 390 MB. * feat(code-mode): explicit per-agent Enable in Settings; no silent chat download Code mode now requires the user to explicitly enable an agent before use, instead of silently downloading a ~200 MB engine on the first chat message. - Settings → Code Mode: each agent shows "Not enabled" + an Enable button that downloads its engine with a live progress indicator (download % → verify → install), then flips to "Engine ready". Driven by a new codeMode:provisionEngine IPC call + a codeMode:engineProgress push channel. The section now states the prerequisite explicitly: the agent must be installed (Enable) and logged in (claude login / codex login — code mode reuses that saved credential). - Chat path no longer auto-downloads: getProvisionedEnginePath() returns the enabled engine or throws a clear "enable it in Settings → Code Mode" error, so there's never a surprise mid-conversation download. getAgentLaunchSpec is sync again. - Agent status: `installed` now means "engine provisioned" (downloaded), driving the Enable/Ready state; the new-session dialog shows "Enable in Settings" and disables un-enabled agents. Dropped the dead PATH-probing for a global CLI. Verified: empty cache -> status installed=false and the chat path throws the enable-in-Settings error (no download); core, renderer, and main typecheck/build; no new lint errors. * fix(code-mode): show only percentage during engine download in Settings * feat(code-mode): prune superseded engine versions after install After a successful provision, remove any other version dirs (and their .meta) for that agent so old ~200 MB engines don't accumulate across version bumps. Best-effort; never fails a good install. Verified: a planted stale version dir + meta are both removed after provisioning the current version. * fix(code-mode): keep showing engine download % after reopening Settings Provisioning state lived in the row component, which unmounts when the Settings dialog closes — so reopening mid-download showed the Enable button again even though the download was still running in the main process. Move provisioning state to a module-level store with one persistent listener on codeMode:engineProgress, so a row remounting (dialog reopened) reflects the live % and resolves to Ready on completion. * fix(code-mode): flip Enable row straight to Ready after install (no Enable flash) On successful provision the in-flight flag was cleared before the async status refresh completed, so the row briefly (or until reopen) showed the Enable button again. Await the status refresh before clearing the flag so it transitions directly to Ready. * fix(code-mode): optimistically show Ready right after Enable completes Awaiting the status refresh wasn't enough — setStatus re-renders the parent separately from the row, leaving a window where the in-flight flag was cleared but the status prop was stale, so the row flashed/stuck on the Enable button until reopen. Track just-enabled agents in a module-level set and treat them as installed immediately; loadStatus still syncs the real status in the background. * fix(code-mode): graft login-shell PATH + add startup deadline #1 (the gh/git "command not found" in packaged builds): GUI/Finder launches inherit launchd's stripped PATH (/usr/bin:/bin:...), so tools the engine spawns — gh, git, rg, bash — fail even though they work from a terminal (e.g. Homebrew's /opt/homebrew/bin/gh). Probe the user's login-shell PATH and graft it onto the engine's env before spawn (shell-env.ts; no-op on Windows / probe failure). #2: add a 60s startup deadline (initialize / session create+load) so a wedged engine fails with a clear, stderr-enriched error instead of an infinite "(pending...)". Overridable via ROWBOAT_ACP_STARTUP_TIMEOUT_MS. Manager now disposes the client on startup failure so the spawned adapter doesn't leak. Verified: getAgentLaunchSpec's env.PATH now includes /opt/homebrew/bin (where gh lives); core builds; no new lint errors. * chore(code-mode): comment out signing/notarization for local builds Revert to the explicit comment-out approach for osxSign/osxNotarize: uncomment them (with APPLE_ID/APPLE_PASSWORD/APPLE_TEAM_ID) for a signed release build. * chore(code-mode): keep signing/notarization active in committed config The repo's forge.config ships with osxSign/osxNotarize enabled (release-ready). Developers comment them out locally for unsigned test builds and don't commit that. * chore: approve workspace build scripts so packaging runs non-interactively The allowBuilds entries were left as "set this to true or false" placeholders, so `pnpm install` / the pre-build deps check aborted with ERR_PNPM_IGNORED_BUILDS and `npm run package` failed. Set them to true (and add node-pty, used by the code-mode embedded terminal) so build scripts are approved and packaging works without a manual `pnpm approve-builds`.
This commit is contained in:
parent
8ce24ebb33
commit
2ddec07712
15 changed files with 1175 additions and 96 deletions
|
|
@ -5,6 +5,92 @@
|
|||
const path = require('path');
|
||||
const pkg = require('./package.json');
|
||||
|
||||
// Stage the ACP coding-adapters (@agentclientprotocol/*-acp) and their full
|
||||
// production dependency closure into the packaged app.
|
||||
//
|
||||
// Why this is needed: code mode spawns each adapter as a SEPARATE `node <entry>`
|
||||
// process and locates it at runtime via require.resolve — so it must ship as a real
|
||||
// on-disk file. esbuild can't inline it (dynamic resolve + spawn target), and Forge
|
||||
// strips the workspace node_modules (see `ignore` below). Without this, packaged
|
||||
// builds throw `Cannot find module '@agentclientprotocol/...'`.
|
||||
//
|
||||
// Why we reconstruct a nested tree instead of copying node_modules: pnpm's store is a
|
||||
// symlink farm that legitimately holds multiple versions of the same package (e.g.
|
||||
// @agentclientprotocol/sdk 0.21 for claude vs 0.22 for codex). We rebuild an npm-style
|
||||
// nested node_modules — dereferencing symlinks and nesting on version conflict — which
|
||||
// resolves correctly regardless of pnpm layout.
|
||||
//
|
||||
// What we DON'T bundle: the agents' native engines (claude / codex, ~200 MB each, shipped
|
||||
// as platform-specific packages). Those are PROVISIONED on demand into
|
||||
// ~/.rowboat/engines/<agent>/<version>/ and the adapters are pointed at them via
|
||||
// CLAUDE_CODE_EXECUTABLE / CODEX_PATH (see packages/core/src/code-mode/acp/). Skipping
|
||||
// them keeps each OS installer ~400 MB smaller while code mode stays fully functional.
|
||||
function stageAcpAdapters(mainDir, destNodeModules) {
|
||||
const fs = require('fs');
|
||||
const ADAPTERS = [
|
||||
'@agentclientprotocol/claude-agent-acp',
|
||||
'@agentclientprotocol/codex-acp',
|
||||
];
|
||||
|
||||
// The native engines, shipped as platform packages. Provisioned on demand instead
|
||||
// (see comment above), so they're excluded from staging.
|
||||
const isNativeEngine = (key) =>
|
||||
/^@anthropic-ai\/claude-agent-sdk-(win32|darwin|linux)/.test(key) || // native claude
|
||||
/^@openai\/codex-(win32|darwin|linux)/.test(key); // native codex
|
||||
|
||||
// Resolve a dependency's real directory by walking node_modules the way Node does,
|
||||
// looking for the package DIRECTORY. We deliberately do NOT use
|
||||
// require.resolve(`${key}/package.json`): that throws for packages whose `exports`
|
||||
// map doesn't expose package.json (e.g. @anthropic-ai/claude-agent-sdk), which would
|
||||
// silently drop them and their subtrees. realpathSync dereferences pnpm's symlinks.
|
||||
// Returns null for deps not installed for this OS (platform-optional binaries).
|
||||
const realDirOf = (key, fromDir) => {
|
||||
let dir = fromDir;
|
||||
for (;;) {
|
||||
const cand = path.join(dir, 'node_modules', ...key.split('/'));
|
||||
if (fs.existsSync(path.join(cand, 'package.json'))) return fs.realpathSync(cand);
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) return null;
|
||||
dir = parent;
|
||||
}
|
||||
};
|
||||
|
||||
let copied = 0;
|
||||
const skippedEngines = new Set();
|
||||
const install = (srcDir, key, destNM, chain) => {
|
||||
const destDir = path.join(destNM, ...key.split('/'));
|
||||
if (fs.existsSync(destDir)) return; // already placed at this exact location
|
||||
if (chain.has(srcDir)) return; // dependency cycle — resolves to ancestor copy
|
||||
fs.mkdirSync(path.dirname(destDir), { recursive: true });
|
||||
fs.cpSync(srcDir, destDir, {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
filter: (s) => path.basename(s) !== 'node_modules', // deps handled by recursion
|
||||
});
|
||||
copied++;
|
||||
const pj = JSON.parse(fs.readFileSync(path.join(srcDir, 'package.json'), 'utf8'));
|
||||
const deps = { ...pj.dependencies, ...pj.optionalDependencies };
|
||||
const nextChain = new Set(chain).add(srcDir);
|
||||
for (const depKey of Object.keys(deps)) {
|
||||
if (isNativeEngine(depKey)) { skippedEngines.add(depKey); continue; }
|
||||
const depDir = realDirOf(depKey, srcDir);
|
||||
if (depDir) install(depDir, depKey, path.join(destDir, 'node_modules'), nextChain);
|
||||
}
|
||||
};
|
||||
|
||||
for (const key of ADAPTERS) {
|
||||
const srcDir = realDirOf(key, mainDir);
|
||||
if (!srcDir) {
|
||||
throw new Error(`ACP adapter '${key}' is not installed in ${mainDir} — run pnpm install`);
|
||||
}
|
||||
install(srcDir, key, destNodeModules, new Set());
|
||||
}
|
||||
if (skippedEngines.size) {
|
||||
console.log(` (skipped native engines — provisioned on demand: ${[...skippedEngines].join(', ')})`);
|
||||
}
|
||||
return copied;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
packagerConfig: {
|
||||
executableName: 'rowboat',
|
||||
|
|
@ -29,19 +115,23 @@ module.exports = {
|
|||
appleIdPassword: process.env.APPLE_PASSWORD,
|
||||
teamId: process.env.APPLE_TEAM_ID
|
||||
},
|
||||
// Since we bundle everything with esbuild, we don't need node_modules at all.
|
||||
// These settings prevent Forge's dependency walker (flora-colossus) from trying
|
||||
// to analyze/copy node_modules, which fails with pnpm's symlinked workspaces.
|
||||
// Regexes are ANCHORED to the app root: .package/node_modules (where
|
||||
// bundle.mjs stages the native node-pty module) must survive packaging.
|
||||
// Since we bundle the main process with esbuild, we don't need the workspace
|
||||
// node_modules. These settings prevent Forge's dependency walker (flora-colossus)
|
||||
// from trying to analyze/copy node_modules, which fails with pnpm's symlinked
|
||||
// workspaces.
|
||||
prune: false,
|
||||
ignore: [
|
||||
/^\/src\//,
|
||||
/^\/node_modules\//,
|
||||
/.gitignore/,
|
||||
/bundle\.mjs/,
|
||||
/tsconfig.json/,
|
||||
],
|
||||
// Strip the workspace src/node_modules (paths are ANCHORED to the app root), BUT
|
||||
// always keep everything under `.package/` — that's our staged output: the
|
||||
// bundled main process, the ACP adapters + their dependency closure (staged by
|
||||
// the generateAssets hook), and the native node-pty module (staged into
|
||||
// .package/node_modules by bundle.mjs). Without the `.package` exemption the
|
||||
// node_modules rule would strip those and code mode / the embedded terminal
|
||||
// would break in packaged builds.
|
||||
ignore: (p) => {
|
||||
if (p === '/.package' || p.startsWith('/.package/')) return false;
|
||||
return [/^\/src\//, /^\/node_modules\//, /\.gitignore/, /bundle\.mjs/, /tsconfig\.json/]
|
||||
.some((re) => re.test(p));
|
||||
},
|
||||
},
|
||||
makers: [
|
||||
{
|
||||
|
|
@ -195,6 +285,15 @@ module.exports = {
|
|||
fs.mkdirSync(rendererDest, { recursive: true });
|
||||
fs.cpSync(rendererSrc, rendererDest, { recursive: true });
|
||||
|
||||
// Stage the ACP coding-adapters (+ their JS dependency closure, minus native
|
||||
// engines) into .package/acp/node_modules. They are spawned as separate node
|
||||
// processes at runtime and Forge strips the workspace node_modules, so they
|
||||
// must be copied in explicitly. See stageAcpAdapters() above for the why.
|
||||
console.log('Staging ACP adapters...');
|
||||
const acpDest = path.join(packageDir, 'acp', 'node_modules');
|
||||
const staged = stageAcpAdapters(__dirname, acpDest);
|
||||
console.log(`✅ Staged ${staged} ACP adapter packages into .package/acp/node_modules`);
|
||||
|
||||
console.log('✅ All assets staged in .package/');
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { IGranolaConfigRepo } from '@x/core/dist/knowledge/granola/repo.js';
|
|||
import { ICodeModeConfigRepo } from '@x/core/dist/code-mode/repo.js';
|
||||
import { CodePermissionRegistry } from '@x/core/dist/code-mode/acp/permission-registry.js';
|
||||
import { checkCodeModeAgentStatus } from '@x/core/dist/code-mode/status.js';
|
||||
import { ensureEngine } from '@x/core/dist/code-mode/acp/engine-provisioner.js';
|
||||
import type { ICodeProjectsRepo } from '@x/core/dist/code-mode/projects/repo.js';
|
||||
import type { ICodeSessionsRepo } from '@x/core/dist/code-mode/sessions/repo.js';
|
||||
import { CodeSessionService } from '@x/core/dist/code-mode/sessions/service.js';
|
||||
|
|
@ -714,6 +715,26 @@ export function setupIpcHandlers() {
|
|||
'codeMode:checkAgentStatus': async () => {
|
||||
return await checkCodeModeAgentStatus();
|
||||
},
|
||||
'codeMode:provisionEngine': async (_event, args) => {
|
||||
// Download + install the agent's engine, streaming progress back to the
|
||||
// requesting window so Settings can show a live bar. 'check' is instant — skip it.
|
||||
try {
|
||||
await ensureEngine(args.agent, {
|
||||
onProgress: (p) => {
|
||||
if (p.phase === 'check') return;
|
||||
_event.sender.send('codeMode:engineProgress', {
|
||||
agent: args.agent,
|
||||
phase: p.phase,
|
||||
receivedBytes: p.receivedBytes,
|
||||
totalBytes: p.totalBytes,
|
||||
});
|
||||
},
|
||||
});
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
||||
}
|
||||
},
|
||||
'codeProject:add': async (_event, args) => {
|
||||
const repo = container.resolve<ICodeProjectsRepo>('codeProjectsRepo');
|
||||
const project = await repo.add(args.path);
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ export function NewSessionDialog({
|
|||
>
|
||||
<div className="font-medium">{a === 'claude' ? 'Claude Code' : 'Codex'}</div>
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
{ready ? 'Ready' : agentStatus?.[a]?.installed ? 'Not signed in' : 'Not installed'}
|
||||
{ready ? 'Ready' : agentStatus?.[a]?.installed ? 'Not signed in' : 'Enable in Settings'}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1759,52 +1759,122 @@ function NoteTaggingSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
type AgentStatus = { installed: boolean; signedIn: boolean }
|
||||
type CodeModeAgentStatus = { claude: AgentStatus; codex: AgentStatus }
|
||||
|
||||
// Engine provisioning runs in the main process and keeps going even if the Settings
|
||||
// dialog is closed. Track its state at MODULE level (not in the row component, which
|
||||
// unmounts on close) so reopening Settings still shows the live % instead of the Enable
|
||||
// button. A single persistent listener on the progress channel feeds this store.
|
||||
type ProvState = { pct: number | null; error?: string }
|
||||
const provStore: Record<string, ProvState | undefined> = {}
|
||||
// Agents we provisioned this session — used to show "Ready" immediately on success
|
||||
// without waiting for the async status refresh to round-trip (which caused the row to
|
||||
// briefly flash the Enable button again).
|
||||
const enabledOptimistic = new Set<string>()
|
||||
const provListeners = new Set<() => void>()
|
||||
let provChannelHooked = false
|
||||
|
||||
function notifyProv() { provListeners.forEach((l) => l()) }
|
||||
|
||||
function startProvisioning(agent: 'claude' | 'codex', onDone: () => void | Promise<void>): void {
|
||||
if (provStore[agent] && !provStore[agent]!.error) return // already in flight
|
||||
provStore[agent] = { pct: null }
|
||||
notifyProv()
|
||||
if (!provChannelHooked) {
|
||||
provChannelHooked = true
|
||||
window.ipc.on('codeMode:engineProgress', (p) => {
|
||||
const cur = provStore[p.agent]
|
||||
if (!cur) return
|
||||
const pct = p.totalBytes ? Math.floor(((p.receivedBytes ?? 0) / p.totalBytes) * 100) : cur.pct
|
||||
provStore[p.agent] = { pct }
|
||||
notifyProv()
|
||||
})
|
||||
}
|
||||
window.ipc.invoke('codeMode:provisionEngine', { agent })
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
// Mark installed optimistically so the row shows "Ready" the instant the flag
|
||||
// clears — don't depend on the async status refresh (which re-renders the parent
|
||||
// separately and left a window showing the Enable button). loadStatus still runs
|
||||
// in the background to sync the real status.
|
||||
enabledOptimistic.add(agent)
|
||||
provStore[agent] = undefined
|
||||
void onDone()
|
||||
} else {
|
||||
provStore[agent] = { pct: null, error: res.error ?? 'Failed to enable' }
|
||||
}
|
||||
})
|
||||
.catch((e) => { provStore[agent] = { pct: null, error: e instanceof Error ? e.message : 'Failed to enable' } })
|
||||
.finally(notifyProv)
|
||||
}
|
||||
|
||||
function useProvisioning(agent: string): ProvState | undefined {
|
||||
const [, force] = useState(0)
|
||||
useEffect(() => {
|
||||
const l = () => force((n) => n + 1)
|
||||
provListeners.add(l)
|
||||
return () => { provListeners.delete(l) }
|
||||
}, [])
|
||||
return provStore[agent]
|
||||
}
|
||||
|
||||
function AgentStatusRow({
|
||||
name,
|
||||
installLink,
|
||||
agent,
|
||||
signInCommand,
|
||||
status,
|
||||
onProvisioned,
|
||||
}: {
|
||||
name: string
|
||||
installLink: string
|
||||
agent: 'claude' | 'codex'
|
||||
signInCommand: string
|
||||
status: AgentStatus | null
|
||||
onProvisioned: () => void
|
||||
}) {
|
||||
const ready = status?.installed && status?.signedIn
|
||||
const needsSignInOnly = status?.installed && !status?.signedIn
|
||||
const prov = useProvisioning(agent)
|
||||
const provisioning = prov !== undefined && prov.error === undefined
|
||||
const error = prov?.error ?? null
|
||||
const enable = useCallback(() => startProvisioning(agent, onProvisioned), [agent, onProvisioned])
|
||||
|
||||
// Treat a just-enabled engine as installed even before the status refresh lands.
|
||||
const installed = (status?.installed ?? false) || enabledOptimistic.has(agent)
|
||||
const ready = installed && status?.signedIn
|
||||
return (
|
||||
<div className="rounded-md border px-3 py-2.5 flex items-center gap-3">
|
||||
<Terminal className="size-4 text-muted-foreground shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium">{name}</div>
|
||||
<div className="text-xs text-muted-foreground mt-0.5 flex items-center gap-3">
|
||||
<span className={cn("inline-flex items-center gap-1", status?.installed ? "text-green-600" : "text-muted-foreground")}>
|
||||
{status?.installed ? <CheckCircle2 className="size-3" /> : <X className="size-3" />}
|
||||
Installed
|
||||
<span className={cn("inline-flex items-center gap-1", installed ? "text-green-600" : "text-muted-foreground")}>
|
||||
{installed ? <CheckCircle2 className="size-3" /> : <X className="size-3" />}
|
||||
{installed ? 'Engine ready' : 'Not enabled'}
|
||||
</span>
|
||||
<span className={cn("inline-flex items-center gap-1", status?.signedIn ? "text-green-600" : "text-muted-foreground")}>
|
||||
{status?.signedIn ? <CheckCircle2 className="size-3" /> : <X className="size-3" />}
|
||||
Signed in
|
||||
</span>
|
||||
</div>
|
||||
{error && <div className="text-xs text-red-600 mt-1 break-words">{error}</div>}
|
||||
</div>
|
||||
{ready ? (
|
||||
{provisioning ? (
|
||||
<span className="inline-flex items-center gap-1.5 text-xs text-muted-foreground shrink-0 tabular-nums">
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
{prov?.pct != null ? `${prov.pct}%` : null}
|
||||
</span>
|
||||
) : ready ? (
|
||||
<span className="rounded-full bg-green-500/10 px-2 py-0.5 text-[10px] font-medium leading-none text-green-600">
|
||||
Ready
|
||||
</span>
|
||||
) : needsSignInOnly ? (
|
||||
) : !installed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={enable}
|
||||
className="rounded-full bg-primary px-3 py-1 text-xs font-medium text-primary-foreground hover:opacity-90 shrink-0"
|
||||
>
|
||||
Enable
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground shrink-0">
|
||||
Run <code className="rounded bg-muted px-1 py-0.5 font-mono text-[11px] text-foreground">{signInCommand}</code>
|
||||
</span>
|
||||
) : (
|
||||
<a
|
||||
href={installLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-primary hover:underline shrink-0"
|
||||
>
|
||||
Install & sign in
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
@ -1907,6 +1977,14 @@ function CodeModeSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
Requires an active <strong className="text-foreground">Claude Code</strong> subscription or
|
||||
a <strong className="text-foreground">ChatGPT/Codex</strong> subscription. You can have one or both.
|
||||
</p>
|
||||
<p>
|
||||
For each agent you want to use, you must have it{' '}
|
||||
<strong className="text-foreground">installed and logged in</strong> on this machine: click{' '}
|
||||
<strong className="text-foreground">Enable</strong> below to download its engine, and sign in by
|
||||
running <code className="rounded bg-muted px-1 py-0.5 font-mono text-[11px] text-foreground">claude login</code>{' '}
|
||||
or <code className="rounded bg-muted px-1 py-0.5 font-mono text-[11px] text-foreground">codex login</code>{' '}
|
||||
in your terminal. Code mode uses that saved login.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
|
|
@ -1924,15 +2002,17 @@ function CodeModeSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
<div className="space-y-2">
|
||||
<AgentStatusRow
|
||||
name="Claude Code"
|
||||
installLink="https://claude.ai/code"
|
||||
agent="claude"
|
||||
signInCommand="claude login"
|
||||
status={status?.claude ?? null}
|
||||
onProvisioned={loadStatus}
|
||||
/>
|
||||
<AgentStatusRow
|
||||
name="Codex"
|
||||
installLink="https://developers.openai.com/codex/cli"
|
||||
agent="codex"
|
||||
signInCommand="codex login"
|
||||
status={status?.codex ?? null}
|
||||
onProvisioned={loadStatus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1984,8 +2064,8 @@ function CodeModeSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
<div className="rounded-md border border-amber-500/40 bg-amber-50/60 dark:bg-amber-950/20 px-3 py-2.5 flex items-start gap-2 text-xs">
|
||||
<AlertTriangle className="size-4 text-amber-600 dark:text-amber-500 shrink-0 mt-0.5" />
|
||||
<div className="text-amber-900 dark:text-amber-200">
|
||||
Neither Claude Code nor Codex is ready. Install at least one and sign in with a subscription
|
||||
account, then click Re-check.
|
||||
Neither Claude Code nor Codex is ready. Click Enable above to download an engine, sign in with a
|
||||
subscription account, then click Re-check.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue