diff --git a/config/packaged-runtime-node-modules.cjs b/config/packaged-runtime-node-modules.cjs index 1eca37b7c05..1ee443f8288 100644 --- a/config/packaged-runtime-node-modules.cjs +++ b/config/packaged-runtime-node-modules.cjs @@ -14,6 +14,7 @@ const projectDir = resolve(__dirname, '..') const requireFromProject = createRequire(join(projectDir, 'package.json')) const PACKAGED_RUNTIME_PACKAGE_ROOTS = [ + '@anthropic-ai/claude-agent-sdk', '@electron-toolkit/utils', '@linear/sdk', '@parcel/watcher', @@ -56,6 +57,11 @@ const ELECTRON_ARCHITECTURE_BY_ENUM = { 4: 'universal' } const PACKAGED_NATIVE_ARCHITECTURES = new Set(['ia32', 'x64', 'arm', 'arm64']) +const PACKAGED_MAIN_REQUIRED_FILES = [ + 'out/main/index.js', + 'out/main/agent-hooks/managed-agent-hook-controls.js' +] +const PACKAGED_MAIN_SOURCE_RE = /^out\/main\/.+\.js$/ const TYPE_DECLARATION_ARTIFACT_RE = /\.d\.(?:c|m)?ts(?:\.map)?$/ const JS_SOURCE_MAP_ARTIFACT_RE = /\.(?:c|m)?js\.map$/ const VERSIONED_ONNXRUNTIME_DYLIB_RE = /^libonnxruntime\.\d[\d.]*\.dylib$/ @@ -223,22 +229,39 @@ function verifyPackagedMainRuntimeDeps(resourcesDir, asar = require('@electron/a return } - const mainFiles = ['out/main/index.js', 'out/main/agent-hooks/managed-agent-hook-controls.js'] const entries = asar.listPackage(asarPath) - const missing = new Set() - - for (const file of mainFiles) { - const entry = findAsarEntry(entries, file) - if (!entry) { + for (const file of PACKAGED_MAIN_REQUIRED_FILES) { + if (!findAsarEntry(entries, file)) { throw new Error(`Packaged main file ${file} was not found in ${asarPath}`) } + } + + const missing = new Set() + // Why every emitted main file rather than the entry points alone: rolldown hoists + // modules shared by two entries into out/main/chunks, so an entry's own bare imports + // move out from under a fixed file list and silently stop being checked. + for (const entry of entries) { + if (!PACKAGED_MAIN_SOURCE_RE.test(normalizeAsarEntryPath(entry))) { + continue + } // Why: @electron/asar lists entries with host separators; Windows returns // backslashes, and extractFile expects that same host-style path. const internalPath = entry.replace(/^[\\/]+/, '') const source = asar.extractFile(asarPath, internalPath).toString('utf8') - for (const match of source.matchAll(/require\(["']([^"']+)["']\)/g)) { - const specifier = match[1] + // Why the lookbehind: Orca has its own registry methods named `require`, so a + // minified `registry.require('some-id')` must not read as a bare specifier. + // Why it readmits `...`: a dot that ends a spread is not member access, and + // the two error directions are not symmetric -- a false positive fails the + // release build loudly, a false negative is this guard going blind. + // Known limit: a specifier inside an embedded source string counts too, and + // ssh-relay-deploy's remote probe names node-pty that way. A remote-only + // dependency added to that script would fail desktop packaging here; telling + // the two apart needs a parser, not a wider pattern. + for (const match of source.matchAll( + /(?:(? { } }) + it('verifies literal dynamic imports from the packaged main bundle', async () => { + const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-dynamic-imports-')) + try { + await writeFile(join(resourcesDir, 'app.asar'), '', 'utf8') + + // The first is the exact shape oxc emits for the memoized SDK import in a + // shipped build; the second is the spaced variant the pattern also accepts. + const sources = new Map([ + [ + 'out/main/index.js', + 'let p=null;function q(){return p??=import(`@anthropic-ai/claude-agent-sdk`),p}' + ], + [ + 'out/main/agent-hooks/managed-agent-hook-controls.js', + 'import (`@anthropic-ai/claude-agent-sdk`)' + ] + ]) + const asar = { + listPackage: () => [...sources.keys()].map((entry) => `/${entry}`), + extractFile: (_asarPath, internalPath) => Buffer.from(sources.get(internalPath), 'utf8') + } + + expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).toThrow( + /@anthropic-ai\/claude-agent-sdk/ + ) + + await mkdir(join(resourcesDir, 'node_modules', '@anthropic-ai', 'claude-agent-sdk'), { + recursive: true + }) + expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).not.toThrow() + } finally { + await rm(resourcesDir, { recursive: true, force: true }) + } + }) + + it('still fails when a required packaged main entry is missing entirely', async () => { + const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-missing-entry-')) + try { + await writeFile(join(resourcesDir, 'app.asar'), '', 'utf8') + + const asar = { + listPackage: () => ['/out/main/index.js'], + extractFile: () => Buffer.from('', 'utf8') + } + + expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).toThrow( + /managed-agent-hook-controls\.js was not found/ + ) + } finally { + await rm(resourcesDir, { recursive: true, force: true }) + } + }) + + it('verifies bare imports that rolldown hoisted into a shared main chunk', async () => { + const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-chunk-imports-')) + try { + await writeFile(join(resourcesDir, 'app.asar'), '', 'utf8') + + // The entry points themselves carry no specifier; only the shared chunk does. + const sources = new Map([ + ['out/main/index.js', ''], + ['out/main/agent-hooks/managed-agent-hook-controls.js', ''], + ['out/main/chunks/managed-agent-hook-controls-CWf8D-KR.js', 'require(`jsonc-parser`)'] + ]) + // Real listPackage emits directory nodes too, and extractFile throws on them, + // so the `.js` anchor is load-bearing -- keep the mock able to catch that. + const directories = ['/out', '/out/main', '/out/main/chunks'] + const asar = { + listPackage: () => [...directories, ...[...sources.keys()].map((entry) => `/${entry}`)], + extractFile: (_asarPath, internalPath) => { + const source = sources.get(internalPath) + if (source === undefined) { + throw new Error(`Expected to find file at: ${internalPath} but found a directory`) + } + return Buffer.from(source, 'utf8') + } + } + + expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).toThrow(/jsonc-parser/) + + await mkdir(join(resourcesDir, 'node_modules', 'jsonc-parser'), { recursive: true }) + expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).not.toThrow() + } finally { + await rm(resourcesDir, { recursive: true, force: true }) + } + }) + + it('reads a spread require, whose leading dots are not member access', async () => { + const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-spread-require-')) + try { + await writeFile(join(resourcesDir, 'app.asar'), '', 'utf8') + + const sources = new Map([ + ['out/main/index.js', 'const all=[...require("jsonc-parser")]'], + ['out/main/agent-hooks/managed-agent-hook-controls.js', ''] + ]) + const asar = { + listPackage: () => [...sources.keys()].map((entry) => `/${entry}`), + extractFile: (_asarPath, internalPath) => Buffer.from(sources.get(internalPath), 'utf8') + } + + expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).toThrow(/jsonc-parser/) + } finally { + await rm(resourcesDir, { recursive: true, force: true }) + } + }) + + it('ignores member calls onto Orca methods that are themselves named require', async () => { + const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-member-require-')) + try { + await writeFile(join(resourcesDir, 'app.asar'), '', 'utf8') + + // electron-sidecar-tab-registry and browser-execution-host-grant-registry both + // expose require(key); a literal key must never read as a packaged specifier. + const sources = new Map([ + ['out/main/index.js', 'registry.require("public-a");grants.require(`host-key`)'], + ['out/main/agent-hooks/managed-agent-hook-controls.js', 'state.import("android-sdk")'] + ]) + const asar = { + listPackage: () => [...sources.keys()].map((entry) => `/${entry}`), + extractFile: (_asarPath, internalPath) => Buffer.from(sources.get(internalPath), 'utf8') + } + + expect(() => verifyPackagedMainRuntimeDeps(resourcesDir, asar)).not.toThrow() + } finally { + await rm(resourcesDir, { recursive: true, force: true }) + } + }) + it('normalizes host-specific asar entry separators', () => { expect(findAsarEntry(['\\out\\main\\index.js'], 'out/main/index.js')).toBe( '\\out\\main\\index.js' @@ -134,6 +263,15 @@ describe('packaged runtime resources', () => { expect(packagedTargets).toContain(join('node_modules', 'proper-lockfile')) }) + it('includes the Claude agent SDK in every desktop package plan', () => { + for (const platform of ['darwin', 'linux', 'win32']) { + const packagedTargets = createPackagedRuntimeNodeModuleResources(platform).map( + (resource) => resource.to + ) + expect(packagedTargets).toContain(join('node_modules', '@anthropic-ai', 'claude-agent-sdk')) + } + }) + it('prunes non-target @parcel/watcher architecture subpackages', async () => { const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-parcel-watcher-prune-')) try { diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-acquisition-options.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-acquisition-options.test.ts index 6340e12a265..8afbdedae8d 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-acquisition-options.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-acquisition-options.test.ts @@ -28,7 +28,8 @@ afterEach(async () => { function attachParams( operationId: string, - expectedRuntimeFence: number | null + expectedRuntimeFence: number | null, + options?: Readonly> ): AgentSessionAttachParams { const params: AgentSessionAttachParams = { envelope: { @@ -47,6 +48,7 @@ function attachParams( agent: 'codex', accountHome: { variable: 'CODEX_HOME', path: '/home/dev/.codex' }, runtimeKind: 'native', + ...(options ? { options } : {}), providerHandle: { kind: 'codex', threadId: 'legacy-thread' } } return { @@ -101,6 +103,70 @@ function expectSettledAttachLease(record: AgentSessionRecord | null): void { } describe('structured session acquisition options', () => { + it('persists create defaults before the first provider acquisition', async () => { + root = await mkdtemp(join(tmpdir(), 'orca-create-options-')) + const store = await AgentSessionRecordStore.open({ + directory: join(root, 'store'), + hostId: 'local' + }) + const sessionAdapter = adapter({ origin: 'created' }) + const options = { model: 'gpt-5.6-sol', effort: 'medium' } + + const created = await performAttach({ + store, + adapter: sessionAdapter, + journalRoot: root, + authority: { + spawnToken: 'spawn-a', + claimKeyId: 'key-1', + handoffOperationId: CREATE_OPERATION, + probe: { outcome: 'reservation-unused' } + }, + callerKey: 'client-1', + params: attachParams(CREATE_OPERATION, null, options), + now: () => NOW, + onAttached: () => {} + }) + + expect(created).toMatchObject({ ok: true }) + expect(sessionAdapter.acquire).toHaveBeenCalledWith(expect.objectContaining({ options })) + expect(store.getRecord(SESSION)?.options).toEqual(options) + }) + + it('replays a create retried after the host re-resolved different options', async () => { + root = await mkdtemp(join(tmpdir(), 'orca-create-retry-')) + const store = await AgentSessionRecordStore.open({ + directory: join(root, 'store'), + hostId: 'local' + }) + const sessionAdapter = adapter({ origin: 'created' }) + const attempt = async (options: Readonly>, spawnToken: string) => + performAttach({ + store, + adapter: sessionAdapter, + journalRoot: root!, + authority: { + spawnToken, + claimKeyId: 'key-1', + handoffOperationId: CREATE_OPERATION, + probe: { outcome: 'reservation-unused' } + }, + callerKey: 'client-1', + params: attachParams(CREATE_OPERATION, null, options), + now: () => NOW, + onAttached: () => {} + }) + + const created = await attempt({ model: 'gpt-5.6-sol', effort: 'medium' }, 'spawn-a') + // Why: the user may reselect a model between an unknown-outcome create and the + // retry that reuses its operation id; the retry must replay, not conflict. + const retried = await attempt({ model: 'gpt-5.5', effort: 'high' }, 'spawn-b') + + expect(created).toMatchObject({ ok: true }) + expect(retried).toMatchObject({ ok: true }) + expect(store.getRecord(SESSION)?.options).toEqual({ model: 'gpt-5.6-sol', effort: 'medium' }) + }) + it('persists provider options before proving a resumed legacy record', async () => { root = await mkdtemp(join(tmpdir(), 'orca-acquisition-options-')) const storeDir = join(root, 'store') diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-attach.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-attach.ts index ce58e31b4ee..59a5bfe0b8e 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-attach.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-attach.ts @@ -54,6 +54,8 @@ export type AgentSessionAttachParams = { agent: AgentSessionHandleProvider accountHome: AgentSessionAccountHome runtimeKind: AgentSessionOwnerRuntimeKind + /** Host-resolved defaults for a create-by-intent; remote attach schemas do not accept them. */ + options?: Readonly> /** Omitted only for create-by-intent; the adapter proves the durable handle. */ providerHandle?: Exclude } @@ -70,7 +72,10 @@ export type AgentSessionAttachAuthority = { /** The fields that define WHICH session this call would attach to. Deliberately * excludes the spawn token and the probe: those differ between a first attempt - * and its retry, and a retry must replay rather than conflict. */ + * and its retry, and a retry must replay rather than conflict. `options` is + * excluded for the same reason — it is the session's initial state, not its + * identity, and the host re-resolves it from settings the user may have changed + * between an unknown-outcome attempt and its retry. */ export function attachFingerprintFields(params: AgentSessionAttachParams): Record { return { location: params.location, @@ -191,6 +196,7 @@ export function reserveRequestFor(input: { location: params.location, provider: params.provider, accountHome: params.accountHome, + ...(params.options ? { options: params.options } : {}), ...(authority.launchArgs ? { launchArgs: authority.launchArgs } : {}), ...(authority.launchEnv ? { launchEnv: authority.launchEnv } : {}), runtimeKind: params.runtimeKind, diff --git a/src/main/runtime/agent-session-reservation-admission.ts b/src/main/runtime/agent-session-reservation-admission.ts index 1735d67e62c..f1be94a2c0e 100644 --- a/src/main/runtime/agent-session-reservation-admission.ts +++ b/src/main/runtime/agent-session-reservation-admission.ts @@ -21,6 +21,7 @@ import { agentSessionExecutionLocationsEqual, isAgentSessionLaunchArgs, isAgentSessionLaunchEnv, + isAgentSessionOptions, type AgentSessionAccountHome, type AgentSessionExecutionLocation, type AgentSessionLaunchArgs, @@ -43,6 +44,8 @@ export type AgentSessionReserveRequest = { launchArgs?: AgentSessionLaunchArgs /** Current launch input validated here but never written to the durable record. */ launchEnv?: AgentSessionLaunchEnv + /** Initial provider options persisted before the first process is acquired. */ + options?: Readonly> runtimeKind: AgentSessionReservation['runtimeKind'] /** Null when the session does not exist yet; otherwise the fence the caller last observed. */ expectedFence: number | null @@ -131,6 +134,9 @@ export function applyAgentSessionReservation( if (request.launchArgs && !isAgentSessionLaunchArgs(request.launchArgs)) { throw new Error('agent_session_launch_args_invalid') } + if (request.options && !isAgentSessionOptions(request.options)) { + throw new Error('agent_session_options_invalid') + } const reservation: AgentSessionReservation = { runtimeKind: request.runtimeKind, spawnToken: @@ -186,6 +192,7 @@ function createAgentSessionRecord( provider: request.provider, providerHandleChain: [], accountHome: request.accountHome, + ...(request.options ? { options: { ...request.options } } : {}), ...(request.launchArgs ? { launchArgs: [...request.launchArgs] } : {}), createdAt: request.now, updatedAt: request.now, diff --git a/src/main/runtime/orca-runtime-resolve-recovered-structured-tui-transcript.ts b/src/main/runtime/orca-runtime-resolve-recovered-structured-tui-transcript.ts index 29e636b8c15..71d15d88945 100644 --- a/src/main/runtime/orca-runtime-resolve-recovered-structured-tui-transcript.ts +++ b/src/main/runtime/orca-runtime-resolve-recovered-structured-tui-transcript.ts @@ -14,6 +14,7 @@ import { getLocalProjectWorktreeGitOptions } from '../project-runtime-git-option import type { AgentSessionAttachParams } from '../native-chat/agent-session-wire/structured-agent-session-attach' import { getSystemCodexHomePath } from '../codex/codex-home-paths' import { resolveTuiAgentLaunchEnv } from '../../shared/tui-agent-launch-defaults' +import { resolveStructuredLaunchSeedOptions } from '../../shared/native-chat-session-option-defaults' import { hasPersistedStructuredAgentSessionStore as hasPersistedStructuredAgentSessionStoreOnDisk } from './structured-agent-session-runtime' import { getProfileUserDataPath } from '../orca-profiles/profile-storage-paths' import { homedir } from 'node:os' @@ -183,6 +184,10 @@ export class OrcaRuntimeWithResolveRecoveredStructuredTuiTranscript extends Orca } const settings = this.requireStore().getSettings() const launchEnv = resolveTuiAgentLaunchEnv(input.agent, settings.agentDefaultEnv) + const options = resolveStructuredLaunchSeedOptions( + settings.nativeChatSessionOptions, + input.agent + ) const location = await this.resolveStructuredAgentSessionLocation(input.worktree) const workspacePath = (await this.resolveRuntimeFileTarget(input.worktree)).worktree.path return { @@ -199,6 +204,7 @@ export class OrcaRuntimeWithResolveRecoveredStructuredTuiTranscript extends Orca variable: input.agent === 'claude' ? 'CLAUDE_CONFIG_DIR' : 'CODEX_HOME', path: await resolveAccountHomePath({ workspacePath, launchEnv, location }) }, + ...(options ? { options } : {}), runtimeKind: 'native' } } diff --git a/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts b/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts index af1fbc20372..9d2c6589508 100644 --- a/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts +++ b/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts @@ -7,7 +7,15 @@ describe('structured agent-session create intent', () => { const runtime = new OrcaRuntimeService( { getSettings: () => ({ - agentDefaultEnv: { codex: { CODEX_HOME: '/configured/home' } } + agentDefaultEnv: { codex: { CODEX_HOME: '/configured/home' } }, + nativeChatSessionOptions: { + codex: { + model: 'gpt-5.6-sol', + valuesByModel: { + 'gpt-5.6-sol': { effort: 'medium', fastMode: true, personality: 'concise' } + } + } + } }) } as never, undefined, @@ -51,6 +59,7 @@ describe('structured agent-session create intent', () => { variable: 'CODEX_HOME', path: '/accounts/selected/home' }) + expect(intent.options).toEqual({ model: 'gpt-5.6-sol', effort: 'medium' }) }) it('pins the configured Claude launch home without Codex launch preparation', async () => { @@ -60,6 +69,12 @@ describe('structured agent-session create intent', () => { getSettings: () => ({ agentDefaultEnv: { claude: { CLAUDE_CONFIG_DIR: '/configured/claude-home' } + }, + nativeChatSessionOptions: { + claude: { + model: 'opus', + valuesByModel: { opus: { effort: 'high', fastMode: true } } + } } }) } as never, @@ -101,6 +116,7 @@ describe('structured agent-session create intent', () => { variable: 'CLAUDE_CONFIG_DIR', path: '/configured/claude-home' }) + expect(intent.options).toEqual({ model: 'opus', effort: 'high' }) }) it('uses the managed Claude launch home before falling back to ~/.claude', async () => { diff --git a/src/main/runtime/rpc/methods/structured-agent-session.test.ts b/src/main/runtime/rpc/methods/structured-agent-session.test.ts index 4af7f165d0f..c43c82d03ee 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session.test.ts @@ -200,6 +200,10 @@ function dispatcher(runtimeOverrides: Record = {}): RpcDispatch variable: params.agent === 'claude' ? 'CLAUDE_CONFIG_DIR' : 'CODEX_HOME', path: params.agent === 'claude' ? '/host/.claude' : '/host/.codex' }, + options: + params.agent === 'claude' + ? { model: 'opus', effort: 'high' } + : { model: 'gpt-5.6-sol', effort: 'medium' }, runtimeKind: 'native' })), publishStructuredAgentSessionTab: vi.fn() @@ -482,7 +486,8 @@ describe('method routing', () => { expect(hostCalls.attach).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ - accountHome: { variable: 'CODEX_HOME', path: '/host/.codex' } + accountHome: { variable: 'CODEX_HOME', path: '/host/.codex' }, + options: { model: 'gpt-5.6-sol', effort: 'medium' } }) ) expect(hostCalls.attach.mock.calls[0]?.[1]).not.toHaveProperty('providerHandle') diff --git a/src/main/runtime/rpc/methods/structured-agent-session.ts b/src/main/runtime/rpc/methods/structured-agent-session.ts index d524c2185df..9ca3c632a83 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session.ts @@ -18,7 +18,10 @@ import { structuredCallerFor as callerFor, supportsStructuredSessions } from './structured-agent-session-gate' -import type { AgentSessionAttachParams } from '../../../native-chat/agent-session-wire/structured-agent-session-attach' +import { + attachFingerprintFields, + type AgentSessionAttachParams +} from '../../../native-chat/agent-session-wire/structured-agent-session-attach' import { STRUCTURED_AGENT_SESSION_HOLD_METHODS } from './structured-agent-session-hold' import { STRUCTURED_AGENT_SESSION_REVEAL_METHODS } from './structured-agent-session-reveal' import { resolveUncommittedStructuredCreate } from './structured-agent-session-precommit-refusal' @@ -111,14 +114,7 @@ export const STRUCTURED_AGENT_SESSION_METHODS: RpcAnyMethod[] = [ const hostFingerprint = computeAgentSessionPayloadFingerprint({ method: 'agentSession.attach', sessionId: params.envelope.sessionId, - fields: { - location: resolved.location, - provider: resolved.provider, - agent: resolved.agent, - accountHome: resolved.accountHome, - runtimeKind: resolved.runtimeKind, - expectedRuntimeFence: null - } + fields: attachFingerprintFields({ ...resolved, envelope: params.envelope }) }) await ensureHostInstalled(ctx) const { agent: _resolvedAgent, provider: _resolvedProvider, ...resolvedAttach } = resolved diff --git a/src/renderer/src/components/right-sidebar/source-control/commit/commit-notices.tsx b/src/renderer/src/components/right-sidebar/source-control/commit/commit-notices.tsx index e46b1f6b3ea..666181a649a 100644 --- a/src/renderer/src/components/right-sidebar/source-control/commit/commit-notices.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/commit/commit-notices.tsx @@ -113,20 +113,20 @@ export function CommitNotices({ role={createPrIntentNotice.tone === 'destructive' ? 'alert' : 'status'} aria-live="polite" className={cn( - 'mt-1 flex min-w-0 items-center gap-1.5 text-[11px]', + 'mt-1 flex min-w-0 flex-col items-start gap-1 text-[11px]', createPrIntentNotice.tone === 'destructive' ? 'text-destructive' : 'text-muted-foreground' )} > {/* Why: Create Review blockers carry recovery steps; truncating hides the action the user needs in a narrow sidebar. */} - + {createPrIntentNotice.message} {createPrIntentNotice.action === 'settings' && onOpenSourceControlAiSettings ? (