Merge remote-tracking branch 'origin/main' into brennanb2025/loop18519

This commit is contained in:
Merge Sim
2026-09-06 03:48:01 -07:00
24 changed files with 572 additions and 52 deletions
+31 -8
View File
@@ -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(
/(?:(?<![.\w])|(?<=\.\.\.))(?:require|import)\s*\(\s*(["'`])([^"'`$]+)\1\s*\)/g
)) {
const specifier = match[2]
if (!isPackagedExternalSpecifier(specifier)) {
continue
}
@@ -44,6 +44,135 @@ describe('packaged runtime resources', () => {
}
})
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 {
@@ -28,7 +28,8 @@ afterEach(async () => {
function attachParams(
operationId: string,
expectedRuntimeFence: number | null
expectedRuntimeFence: number | null,
options?: Readonly<Record<string, string>>
): 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<Record<string, string>>, 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')
@@ -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<Record<string, string>>
/** Omitted only for create-by-intent; the adapter proves the durable handle. */
providerHandle?: Exclude<AgentSessionProviderHandle, { kind: 'opaque' }>
}
@@ -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<string, unknown> {
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,
@@ -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<Record<string, string>>
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,
@@ -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'
}
}
@@ -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 () => {
@@ -200,6 +200,10 @@ function dispatcher(runtimeOverrides: Record<string, unknown> = {}): 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')
@@ -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
@@ -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. */}
<span className="min-w-0 flex-1 break-words leading-4 [overflow-wrap:anywhere]">
<span className="min-w-0 self-stretch break-words leading-4 [overflow-wrap:anywhere]">
{createPrIntentNotice.message}
</span>
{createPrIntentNotice.action === 'settings' && onOpenSourceControlAiSettings ? (
<button
type="button"
className="shrink-0 font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
className="text-left font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
onClick={() => onOpenSourceControlAiSettings()}
>
{translate(
@@ -30,6 +30,9 @@ describe('resolveAgentLaunchRoute', () => {
'routes a supported local %s launch to structured native chat',
(agent) => {
expect(route({ agent })).toBe('structured-native-chat')
expect(route({ agent, initialSessionOptions: { model: 'gpt-5.6-sol' } })).toBe(
'structured-native-chat'
)
expect(
route({ agent, launchText: 'explain this change', promptDelivery: 'auto-submit' })
).toBe('structured-native-chat')
@@ -76,7 +79,6 @@ describe('resolveAgentLaunchRoute', () => {
expect(route({ agent: 'openclaude' })).toBe('legacy-native-chat')
expect(route({ agent: 'grok' })).toBe('legacy-native-chat')
expect(route({ requiresTuiLaunchCustomization: true })).toBe('legacy-native-chat')
expect(route({ initialSessionOptions: { model: 'gpt-5.6-sol' } })).toBe('legacy-native-chat')
})
it.each([
@@ -88,15 +88,11 @@ export function resolveAgentLaunchRoute(input: AgentLaunchRoutingInput): AgentLa
const projectRuntime = input.projectRuntime
const runtimeRefused =
projectRuntime?.status === 'repair-required' || projectRuntime?.runtime.kind === 'wsl'
const hasInitialSessionOptions = Boolean(
input.initialSessionOptions && Object.keys(input.initialSessionOptions).length > 0
)
const structuredSupported =
isAgentSessionHandleProvider(input.agent) &&
input.promptDelivery !== 'draft' &&
input.workspaceKind !== 'floating' &&
input.requiresTuiLaunchCustomization !== true &&
!hasInitialSessionOptions &&
input.executionHostId === 'local' &&
// Windows eligibility is not a client-side platform guess for either provider: the executing
// host measures it at create time (agentSession.createSupport), because only that host knows
@@ -43,7 +43,13 @@ const store = {
activeRuntimeEnvironmentId: null,
experimentalNativeChat: true,
experimentalStructuredNativeChat: true,
openAgentTabsInChatByDefault: true
openAgentTabsInChatByDefault: true,
nativeChatSessionOptions: undefined as
| Record<
string,
{ model?: string; valuesByModel?: Record<string, Record<string, string | boolean>> }
>
| undefined
},
projects: [{ id: 'repo-1', localWindowsRuntimePreference: { kind: 'inherit-global' as const } }],
repos: [{ id: 'repo-1', connectionId: null as string | null, path: '/repo' }],
@@ -153,6 +159,7 @@ describe('structured chat adoption guard on the launch path', () => {
mockToastError.mockReset()
hostCapabilities = STRUCTURED_HOST_CAPABILITIES
store.settings.openAgentTabsInChatByDefault = true
store.settings.nativeChatSessionOptions = undefined
})
it('takes the structured path when the chat-default view is selected', async () => {
@@ -175,6 +182,23 @@ describe('structured chat adoption guard on the launch path', () => {
expect(mockWaitForAgentReady).not.toHaveBeenCalled()
})
// Routing only: the host seeds the saved values, so preservation is pinned there.
it('takes the structured path when a Codex model and effort are already saved', async () => {
store.settings.nativeChatSessionOptions = {
codex: {
model: 'gpt-5.6-sol',
valuesByModel: { 'gpt-5.6-sol': { effort: 'medium' } }
}
}
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
const result = launchAgentInNewTab({ agent: 'codex', worktreeId: 'wt-1' })
expect(result).toMatchObject({ tabId: null, focusAfterMenuClose: 'structured-session' })
expect(mockCreateStructuredCodexSessionLaunchIntent).toHaveBeenCalledWith('wt-1', 'codex')
expect(mockCreateTab).not.toHaveBeenCalled()
})
it('takes the structured path for Claude, naming Claude as the create provider', async () => {
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
+1 -1
View File
@@ -221,7 +221,7 @@ function isAgentSessionAccountHome(value: unknown): value is AgentSessionAccount
)
}
function isAgentSessionOptions(value: unknown): value is Record<string, string> {
export function isAgentSessionOptions(value: unknown): value is Record<string, string> {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
return false
}
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import {
clearNativeChatSessionOptionModel,
resolveNativeChatSessionOptionDefaults,
resolveStructuredLaunchSeedOptions,
updateNativeChatSessionOptionDefaults
} from './native-chat-session-option-defaults'
import type { PersistedNativeChatSessionOptions } from './native-chat-session-options'
@@ -74,3 +75,66 @@ describe('resolveNativeChatSessionOptionDefaults', () => {
})
})
})
describe('resolveStructuredLaunchSeedOptions', () => {
const persistedCodex = (
valuesByModel: Record<string, Record<string, string | boolean>>
): PersistedNativeChatSessionOptions =>
({ codex: { model: 'gpt-5.6-sol', valuesByModel } }) as PersistedNativeChatSessionOptions
it('seeds the saved model and effort a structured create must apply', () => {
expect(
resolveStructuredLaunchSeedOptions(
persistedCodex({ 'gpt-5.6-sol': { effort: 'medium' } }),
'codex'
)
).toEqual({ model: 'gpt-5.6-sol', effort: 'medium' })
})
it('drops ids the providers only accept mid-session', () => {
// `fastMode` is a boolean and `personality` is settable only mid-session;
// neither belongs in the reservation's Record<string, string>.
expect(
resolveStructuredLaunchSeedOptions(
persistedCodex({
'gpt-5.6-sol': { effort: 'high', fastMode: true, personality: 'concise' }
}),
'codex'
)
).toEqual({ model: 'gpt-5.6-sol', effort: 'high' })
})
it('drops a seeded id whose persisted value is not a usable string', () => {
// settings.json is user-writable, so a non-string `effort` must not reach a
// record typed Record<string, string> and be emitted as a turn option.
expect(
resolveStructuredLaunchSeedOptions(
persistedCodex({ 'gpt-5.6-sol': { effort: true } }),
'codex'
)
).toEqual({ model: 'gpt-5.6-sol' })
expect(
resolveStructuredLaunchSeedOptions(
persistedCodex({ 'gpt-5.6-sol': { effort: ' ' } }),
'codex'
)
).toEqual({ model: 'gpt-5.6-sol' })
})
it('seeds nothing when the stored values empty the model out', () => {
// `valuesByModel` is merged over the resolved model, so a stored `model` key
// can blank it. Emitting `{ model: '' }` fails the record's bounded-string
// guard, and that throw is not a wire refusal code — it escapes as a raw
// error the client reads as unknown, stranding the launch with no fallback.
expect(
resolveStructuredLaunchSeedOptions(persistedCodex({ 'gpt-5.6-sol': { model: '' } }), 'codex')
).toBeUndefined()
})
it('seeds nothing until a model is picked, so the CLI default survives', () => {
expect(resolveStructuredLaunchSeedOptions(undefined, 'codex')).toBeUndefined()
expect(
resolveStructuredLaunchSeedOptions({ codex: { valuesByModel: {} } }, 'codex')
).toBeUndefined()
})
})
@@ -28,6 +28,33 @@ export function resolveNativeChatSessionOptionDefaults(
return values
}
/** Why only these two: they are the only ids the picker persists into
* `nativeChatSessionOptions` that both structured providers also accept as
* strings. Claude's `fastMode` is a boolean the durable `Record<string, string>`
* record cannot carry, and the providers' remaining keys are settable only
* mid-session, never seeded at launch. */
const STRUCTURED_LAUNCH_SEED_OPTION_IDS = ['model', 'effort'] as const
/** The saved selection a structured create seeds into its reservation, narrowed
* to the wire-safe string subset the durable record and both providers accept. */
export function resolveStructuredLaunchSeedOptions(
persisted: PersistedNativeChatSessionOptions | null | undefined,
agent: AgentType
): Record<string, string> | undefined {
const defaults = resolveNativeChatSessionOptionDefaults(persisted, agent)
if (!defaults) {
return undefined
}
const seeded: Record<string, string> = {}
for (const id of STRUCTURED_LAUNCH_SEED_OPTION_IDS) {
const value = defaults[id]
if (typeof value === 'string' && value.trim()) {
seeded[id] = value
}
}
return Object.keys(seeded).length > 0 ? seeded : undefined
}
/** Why: an authoritative probe proved this id gone, and a stale `model` is emitted
* verbatim as a launch flag — grok exits fatally on an unknown one. Dropping only
* `model` keeps the per-model option values for a later reselect. */
+9 -2
View File
@@ -3,8 +3,15 @@
const READY_MARKER = 'GOLDEN_STUB_AGENT_READY'
const EXIT_MARKER = 'GOLDEN_STUB_AGENT_EXITED'
// The interactive fixture does not implement Codex's JSONL app-server API.
if (process.argv[2] === 'app-server') {
process.stderr.write("error: unrecognized subcommand 'app-server'\n")
process.exit(2)
}
const ESC = '\x1b'
const keyboardProtocolMode = process.argv.includes('--keyboard-protocol')
const keyboardProtocolAgent = process.argv.includes('--grok') ? 'Grok' : 'Codex'
// Both match the bytes after ESC, so the control character stays out of the
// pattern: a CSI/SS3 introducer still missing its final byte, and a complete
// CSI/SS3 sequence. Shift+Enter is matched before either is consulted.
@@ -20,7 +27,7 @@ function render() {
const lines = composer.split('\n')
const renderedComposer = lines.map((line, index) => `${index === 0 ? '> ' : ' '}${line}`)
process.stdout.write(
`${keyboardProtocolMode ? '\x1b]0;\u280b Codex is thinking\x07\x1b[>1u' : '\x1b]0;Golden Stub Agent\x07'}${[
`${keyboardProtocolMode ? `\x1b]0;\u280b ${keyboardProtocolAgent} is thinking\x07\x1b[>1u` : '\x1b]0;Golden Stub Agent\x07'}${[
'\x1b[H\x1b[2JGolden Stub Agent',
`[${READY_MARKER}]`,
'',
@@ -40,7 +47,7 @@ function exitCleanly() {
if (process.stdin.isTTY) {
process.stdin.setRawMode(false)
}
const idleTitle = keyboardProtocolMode ? '\x1b]0;Codex\x07' : ''
const idleTitle = keyboardProtocolMode ? `\x1b]0;${keyboardProtocolAgent}\x07` : ''
process.stdout.write(`${idleTitle}\x1b[?1049l[${EXIT_MARKER}]\r\n`, () => process.exit(0))
}
+3 -3
View File
@@ -10,7 +10,7 @@ import { readFileSync, existsSync, realpathSync, rmSync } from 'node:fs'
import { TEST_REPO_PATH_FILE } from './global-setup'
export function linkedWorktreePaths(testRepoDir: string): string[] {
const root = realpathSync(testRepoDir)
const root = realpathSync.native(testRepoDir)
const output = execFileSync('git', ['-C', testRepoDir, 'worktree', 'list', '--porcelain'], {
encoding: 'utf8'
})
@@ -23,7 +23,7 @@ export function linkedWorktreePaths(testRepoDir: string): string[] {
if (!existsSync(recordedPath)) {
continue
}
const canonicalPath = realpathSync(recordedPath)
const canonicalPath = realpathSync.native(recordedPath)
if (canonicalPath !== root) {
linked.add(canonicalPath)
}
@@ -32,7 +32,7 @@ export function linkedWorktreePaths(testRepoDir: string): string[] {
}
export function cleanupTestRepository(testRepoDir: string): void {
const root = realpathSync(testRepoDir)
const root = realpathSync.native(testRepoDir)
let worktreePaths: string[] = []
try {
worktreePaths = linkedWorktreePaths(root)
+1 -1
View File
@@ -39,7 +39,7 @@ describe('E2E global teardown ownership', () => {
git(repoPath, ['worktree', 'add', '-b', 'second-owned', secondWorktreePath])
expect(new Set(linkedWorktreePaths(repoPath))).toEqual(
new Set([realpathSync(firstWorktreePath), realpathSync(secondWorktreePath)])
new Set([realpathSync.native(firstWorktreePath), realpathSync.native(secondWorktreePath)])
)
cleanupTestRepository(repoPath)
+1 -1
View File
@@ -25,7 +25,7 @@ export function getGoldenStubAgentLaunchEnv(): NodeJS.ProcessEnv {
export async function configureGoldenStubAgent(
page: Page,
options: {
agent?: (typeof GOLDEN_STUB_AGENTS)[number]['id']
agent?: (typeof GOLDEN_STUB_AGENTS)[number]['id'] | 'grok'
agentArgs?: string
/** Windows default shell the launch command must survive; ignored elsewhere. */
windowsShell?: BuiltInWindowsTerminalShell
+1 -1
View File
@@ -41,7 +41,7 @@ const BACKUP_EXISTING_STUB_SCRIPT =
// The marker is written first so stale-lock recovery only removes a stub this helper wrote.
const STAGE_SCRIPT =
`mkdir -p /usr/local/bin && : > ${WSL_STUB_STAGED_MARKER} && ` +
`printf '#!/bin/sh\\necho GOLDEN_STUB_AGENT_READY\\nexec sleep 3600\\n' > ${WSL_STUB_PATH} && ` +
`printf '#!/bin/sh\\nif [ "$1" = app-server ]; then echo "error: unrecognized subcommand app-server" >&2; exit 2; fi\\necho GOLDEN_STUB_AGENT_READY\\nexec sleep 3600\\n' > ${WSL_STUB_PATH} && ` +
`chmod 0755 ${WSL_STUB_PATH}`
// The marker is written before the link so a crashed run over-reports rather than leaks a link.
@@ -0,0 +1,91 @@
/**
* The Create PR intent notice pairs a wrapping sentence with a "Source Control
* AI settings" link. In a minimum-width sidebar the link must not share the
* message's row, or it squeezes the sentence into a one-word-per-line column.
*/
import { test, expect } from './helpers/orca-app'
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import {
createStagedCommitMessageChange,
openSourceControl,
seedCreatePrComposer
} from './helpers/source-control-ai-generation'
import { RIGHT_SIDEBAR_MIN_WIDTH } from '../../src/renderer/src/components/right-sidebar/right-sidebar-width'
test.describe('Source Control Create PR intent notice layout', () => {
test('keeps the settings link off the message row at the minimum sidebar width', async ({
orcaPage
}) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
const { prWorktreeId, prWorktreePath, primaryBranch } = await seedCreatePrComposer(orcaPage)
// A real staged change with no commit draft is what routes the intent run
// into the "configure Source Control AI" notice, which carries the link.
createStagedCommitMessageChange(prWorktreePath)
await orcaPage.evaluate(
({ prWorktreeId, primaryBranch }) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
store.setState((current) => ({
// Unconfigured Source Control AI is what routes the run into the
// "configure Source Control AI" notice rather than a failure notice.
settings: current.settings
? { ...current.settings, sourceControlAi: undefined, commitMessageAi: undefined }
: current.settings,
repos: current.repos.map((repo) => ({ ...repo, sourceControlAi: undefined })),
// Blocked-on-push keeps "Create PR" running the intent flow instead of
// opening the composer form.
getHostedReviewCreationEligibility: async () => ({
provider: 'github' as const,
review: null,
reviewLookupOutcome: 'not_found' as const,
canCreate: false,
blockedReason: 'needs_push' as const,
nextAction: 'push' as const,
defaultBaseRef: primaryBranch,
head: 'e2e-secondary'
}),
fetchHostedReviewForBranch: async () => null,
fetchPRForBranch: async () => null,
pushBranch: async (worktreeId: string) => {
if (worktreeId !== prWorktreeId) {
throw new Error(`Create PR intent pushed unexpected worktree ${worktreeId}`)
}
}
}))
},
{ prWorktreeId, primaryBranch }
)
await openSourceControl(orcaPage, prWorktreeId)
await orcaPage.evaluate((minWidth) => {
window.__store?.getState().setRightSidebarWidth(minWidth)
}, RIGHT_SIDEBAR_MIN_WIDTH)
const createPr = orcaPage.getByRole('button', { name: 'Create PR' }).first()
await expect(createPr).toBeVisible({ timeout: 10_000 })
await expect(createPr).toBeEnabled()
await createPr.click()
const notice = orcaPage.locator('#commit-area-create-pr-intent')
const settingsLink = notice.getByRole('button', { name: 'Source Control AI settings' })
await expect(settingsLink).toBeVisible({ timeout: 20_000 })
if (process.env.ORCA_PR_INTENT_NOTICE_SCREENSHOT_PATH) {
await orcaPage.evaluate(() => document.documentElement.classList.add('dark'))
await notice.screenshot({ path: process.env.ORCA_PR_INTENT_NOTICE_SCREENSHOT_PATH })
}
// The layout contract: the link starts below the message's last line.
const messageBox = await notice.locator('span').first().boundingBox()
const linkBox = await settingsLink.boundingBox()
expect(messageBox).not.toBeNull()
expect(linkBox).not.toBeNull()
expect(linkBox!.y).toBeGreaterThanOrEqual(messageBox!.y + messageBox!.height)
// A squeezed message wraps far taller than the ~4 lines this sentence needs.
expect(messageBox!.height).toBeLessThan(70)
})
})
@@ -54,13 +54,19 @@ test('resets standard keyboard bytes after a protocol-mode agent exits on ConPTY
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await configureGoldenStubAgent(orcaPage, { agentArgs: '--keyboard-protocol' })
await launchGoldenStubAgentFromNewTab(orcaPage)
// Grok is the supported native ConPTY exception to Kitty protocol withholding.
await configureGoldenStubAgent(orcaPage, {
agent: 'grok',
agentArgs: '--keyboard-protocol --grok'
})
await launchGoldenStubAgentFromNewTab(orcaPage, /^Grok(?:\s|$)/i)
const ptyId = await waitForActivePanePtyId(orcaPage)
await expect.poll(() => getKittyKeyboardFlags(orcaPage), { timeout: 10_000 }).toBe(1)
await clearTerminalPtyWriteLog(electronApp)
// Kitty flag 1 preserves plain Enter; modified Enter proves CSI-u input.
await orcaPage.keyboard.press('Shift+Enter')
await orcaPage.keyboard.type('exit')
await orcaPage.keyboard.press('Enter')
await waitForTerminalOutput(orcaPage, GOLDEN_STUB_EXIT_MARKER, 15_000)
@@ -68,7 +74,8 @@ test('resets standard keyboard bytes after a protocol-mode agent exits on ConPTY
.filter((entry) => entry.id === ptyId)
.map((entry) => entry.data)
.join('')
expect(protocolWrites.includes('\x1b[13u') || protocolWrites.includes('\x1b[13;1u')).toBe(true)
expect(protocolWrites).toContain('\x1b[13;2u')
expect(protocolWrites).toContain('\r')
await expect.poll(() => getKittyKeyboardFlags(orcaPage), { timeout: 10_000 }).toBe(0)
await clearTerminalPtyWriteLog(electronApp)
+49 -10
View File
@@ -1,4 +1,5 @@
import { test, expect } from './helpers/orca-app'
import { getFirstWslDistro, useWslRuntimeForActiveProject } from './helpers/wsl-golden-stub-agent'
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import {
execInTerminal,
@@ -27,7 +28,9 @@ test.describe('Windows terminal env and shell identity', () => {
await waitForTerminalOutput(orcaPage, marker, 15_000)
})
test('Windows tab icons stay pinned to the shell used at tab creation', async ({ orcaPage }) => {
test('native Windows tab icons stay pinned to the effective shell at tab creation', async ({
orcaPage
}) => {
test.skip(process.platform !== 'win32', 'Windows shell icons only render on Windows')
const tabIds = await orcaPage.evaluate(() => {
@@ -41,10 +44,11 @@ test.describe('Windows terminal env and shell identity', () => {
throw new Error('No active worktree')
}
// Native project ownership makes a global WSL shell fall back to PowerShell.
store.setState({
settings: { ...state.settings!, terminalWindowsShell: 'wsl.exe' }
})
const wslTab = store.getState().createTab(worktreeId, undefined, undefined, {
const fallbackTab = store.getState().createTab(worktreeId, undefined, undefined, {
activate: false
})
@@ -55,33 +59,68 @@ test.describe('Windows terminal env and shell identity', () => {
activate: false
})
return { wslTabId: wslTab.id, cmdTabId: cmdTab.id }
return { fallbackTabId: fallbackTab.id, cmdTabId: cmdTab.id }
})
const tabSnapshot = await orcaPage.evaluate(({ wslTabId, cmdTabId }) => {
const tabSnapshot = await orcaPage.evaluate(({ fallbackTabId, cmdTabId }) => {
const state = window.__store!.getState()
const tabs = Object.values(state.tabsByWorktree).flat()
return {
wslShell: tabs.find((tab) => tab.id === wslTabId)?.shellOverride,
fallbackShell: tabs.find((tab) => tab.id === fallbackTabId)?.shellOverride,
cmdShell: tabs.find((tab) => tab.id === cmdTabId)?.shellOverride
}
}, tabIds)
expect(tabSnapshot).toEqual({
wslShell: 'wsl.exe',
fallbackShell: 'powershell.exe',
cmdShell: 'cmd.exe'
})
const wslTab = orcaPage.locator(
`[data-testid="sortable-tab"][data-tab-id="${tabIds.wslTabId}"]`
const fallbackTab = orcaPage.locator(
`[data-testid="sortable-tab"][data-tab-id="${tabIds.fallbackTabId}"]`
)
const cmdTab = orcaPage.locator(
`[data-testid="sortable-tab"][data-tab-id="${tabIds.cmdTabId}"]`
)
await expect(wslTab).toBeVisible()
await expect(fallbackTab).toBeVisible()
await expect(cmdTab).toBeVisible()
await expect(wslTab.locator('[data-shell-icon]')).toHaveAttribute('data-shell-icon', 'wsl.exe')
await expect(fallbackTab.locator('[data-shell-icon]')).toHaveAttribute(
'data-shell-icon',
'powershell.exe'
)
await expect(cmdTab.locator('[data-shell-icon]')).toHaveAttribute('data-shell-icon', 'cmd.exe')
})
test('WSL project tab icons retain runtime ownership across global shell changes', async ({
orcaPage
}) => {
test.skip(process.platform !== 'win32', 'WSL shell icons require Windows')
const distro = await getFirstWslDistro(orcaPage)
test.skip(!distro, 'WSL icon coverage requires an installed distro')
await useWslRuntimeForActiveProject(orcaPage, distro!)
const tabIds = await orcaPage.evaluate(async () => {
const store = window.__store!
const worktreeId = store.getState().activeWorktreeId!
const ids: string[] = []
for (const shell of ['powershell.exe', 'cmd.exe'] as const) {
await store.getState().updateSettings({ terminalWindowsShell: shell })
ids.push(
store.getState().createTab(worktreeId, undefined, undefined, { activate: false }).id
)
}
return ids
})
const shells = await orcaPage.evaluate((ids) => {
const tabs = Object.values(window.__store!.getState().tabsByWorktree).flat()
return ids.map((id) => tabs.find((tab) => tab.id === id)?.shellOverride)
}, tabIds)
expect(shells).toEqual(['wsl.exe', 'wsl.exe'])
for (const id of tabIds) {
const tab = orcaPage.locator(`[data-testid="sortable-tab"][data-tab-id="${id}"]`)
await expect(tab).toBeVisible()
await expect(tab.locator('[data-shell-icon]')).toHaveAttribute('data-shell-icon', 'wsl.exe')
}
})
})