Fix WSL agent setup to use the distro login shell (#5285)

This commit is contained in:
Jinwoo Hong
2026-06-12 17:01:34 -04:00
committed by GitHub
parent ebeb12fedc
commit 5f3b61aa65
19 changed files with 990 additions and 676 deletions
+7 -54
View File
@@ -1015,12 +1015,7 @@ describe('createPtySubprocess', () => {
expect(spawnMock).toHaveBeenCalledWith(
'wsl.exe',
[
'--',
'bash',
'-c',
`cd '${expectedLinuxCwd}' && export PATH="$HOME/.local/bin:$PATH" && exec bash -l`
],
['--', 'sh', '-c', expect.stringContaining(`cd '${expectedLinuxCwd}'`)],
expect.objectContaining({ cwd: expect.any(String) })
)
})
@@ -1057,14 +1052,7 @@ describe('createPtySubprocess', () => {
expect(spawnMock).toHaveBeenCalledWith(
'wsl.exe',
[
'-d',
'Debian',
'--',
'bash',
'-c',
`cd '${expectedLinuxCwd}' && export PATH="$HOME/.local/bin:$PATH" && exec bash -l`
],
['-d', 'Debian', '--', 'sh', '-c', expect.stringContaining(`cd '${expectedLinuxCwd}'`)],
expect.objectContaining({ cwd: expect.any(String) })
)
})
@@ -1092,14 +1080,7 @@ describe('createPtySubprocess', () => {
expect(spawnMock).toHaveBeenCalledWith(
'wsl.exe',
[
'-d',
'Ubuntu',
'--',
'bash',
'-c',
'cd \'/home/jin/repo\' && export PATH="$HOME/.local/bin:$PATH" && exec bash -l'
],
['-d', 'Ubuntu', '--', 'sh', '-c', expect.stringContaining("cd '/home/jin/repo'")],
expect.objectContaining({ cwd: expect.any(String) })
)
})
@@ -1127,14 +1108,7 @@ describe('createPtySubprocess', () => {
expect(spawnMock).toHaveBeenCalledWith(
'wsl.exe',
[
'-d',
'Ubuntu',
'--',
'bash',
'-c',
'cd \'/home/jin/repo\' && export PATH="$HOME/.local/bin:$PATH" && exec bash -l'
],
['-d', 'Ubuntu', '--', 'sh', '-c', expect.stringContaining("cd '/home/jin/repo'")],
expect.objectContaining({
env: expect.not.objectContaining({
CODEX_HOME: expect.anything(),
@@ -1219,14 +1193,7 @@ describe('createPtySubprocess', () => {
expect(spawnMock).toHaveBeenCalledWith(
'wsl.exe',
[
'-d',
'Ubuntu',
'--',
'bash',
'-c',
`cd '${expectedLinuxCwd}' && export PATH="$HOME/.local/bin:$PATH" && exec bash -l`
],
['-d', 'Ubuntu', '--', 'sh', '-c', expect.stringContaining(`cd '${expectedLinuxCwd}'`)],
expect.objectContaining({
env: expect.objectContaining({
CODEX_HOME: '/home/jin/.local/share/orca/codex-accounts/a/home',
@@ -1260,14 +1227,7 @@ describe('createPtySubprocess', () => {
expect(spawnMock).toHaveBeenCalledWith(
'wsl.exe',
[
'-d',
'Ubuntu',
'--',
'bash',
'-c',
'cd \'/home/jin/repo\' && export PATH="$HOME/.local/bin:$PATH" && exec bash -l'
],
['-d', 'Ubuntu', '--', 'sh', '-c', expect.stringContaining("cd '/home/jin/repo'")],
expect.objectContaining({
env: expect.objectContaining({ CODEX_HOME: '/home/jin/.codex-alt' })
})
@@ -1350,14 +1310,7 @@ describe('createPtySubprocess', () => {
)
expect(spawnMock).toHaveBeenCalledWith(
'wsl.exe',
[
'-d',
'Ubuntu',
'--',
'bash',
'-c',
'cd \'/home/jin/repo/subdir\' && export PATH="$HOME/.local/bin:$PATH" && exec bash -l'
],
['-d', 'Ubuntu', '--', 'sh', '-c', expect.stringContaining("cd '/home/jin/repo/subdir'")],
expect.objectContaining({ cwd: expect.any(String) })
)
})
+13 -3
View File
@@ -1,6 +1,10 @@
import { execFile } from 'child_process'
import { promisify } from 'util'
import path from 'path'
import {
buildWslLoginShellCommand,
escapeWslShCommandForWindows
} from '../../shared/wsl-login-shell-command'
const execFileAsync = promisify(execFile)
const WSL_AGENT_DETECTION_TIMEOUT_MS = 10000
@@ -30,8 +34,8 @@ export async function detectWslCommandsOnPath(
try {
// Why: WSL cold-start plus many parallel wsl.exe probes can timeout and
// cache an empty result. One interactive probe matches user terminals and
// gives the distro a single startup path.
// cache an empty result. One probe through the distro user's login shell
// matches zsh/bash PATH customizations from their normal terminals.
const { stdout } = await execWslAgentDetectionCommand(wslTarget, script)
return parseWslDetectedCommands(stdout)
} catch {
@@ -50,7 +54,13 @@ async function execWslAgentDetectionCommand(
const distroArgs = target.distro ? ['-d', target.distro] : []
const commandPromise = execFileAsync(
'wsl.exe',
[...distroArgs, '--exec', 'bash', '-ic', command],
[
...distroArgs,
'--',
'sh',
'-c',
escapeWslShCommandForWindows(buildWslLoginShellCommand(command))
],
{
encoding: 'utf-8',
timeout: WSL_AGENT_DETECTION_TIMEOUT_MS
+33
View File
@@ -0,0 +1,33 @@
import { execFile } from 'child_process'
import { promisify } from 'util'
import {
buildWslLoginShellCommand,
escapeWslShCommandForWindows
} from '../../shared/wsl-login-shell-command'
import type { WslPreflightTarget } from './preflight-wsl-agent-detection'
const execFileAsync = promisify(execFile)
export type PreflightWslCommandResult = { stdout: string; stderr: string }
export function runPreflightCommandInWsl(
target: WslPreflightTarget,
command: string,
timeoutMs: number
): Promise<PreflightWslCommandResult> {
const distroArgs = target.distro ? ['-d', target.distro] : []
return execFileAsync(
'wsl.exe',
[
...distroArgs,
'--',
'sh',
'-c',
escapeWslShCommandForWindows(buildWslLoginShellCommand(command))
],
{
encoding: 'utf-8',
timeout: timeoutMs
}
) as Promise<PreflightWslCommandResult>
}
+18 -10
View File
@@ -278,10 +278,10 @@ describe('preflight', () => {
}
if (command === 'wsl.exe') {
const script = String(args[5])
if (script === "'gh' --version") {
if (script.includes('gh') && script.includes('--version')) {
return { stdout: 'gh version 2.0.0\n' }
}
if (script === "'gh' auth status") {
if (script.includes('gh') && script.includes('auth status')) {
return { stdout: 'github.com\n - Active account: true\n' }
}
throw new Error(`unexpected WSL script ${script}`)
@@ -294,12 +294,12 @@ describe('preflight', () => {
expect(status.gh).toEqual({ installed: true, authenticated: true })
expect(execFileAsyncMock).toHaveBeenCalledWith(
'wsl.exe',
['-d', 'Ubuntu', '--', 'bash', '-lc', "'gh' --version"],
['-d', 'Ubuntu', '--', 'sh', '-c', expect.stringMatching(/gh[\s\S]*--version/)],
{ encoding: 'utf-8', timeout: 5000 }
)
expect(execFileAsyncMock).toHaveBeenCalledWith(
'wsl.exe',
['-d', 'Ubuntu', '--', 'bash', '-lc', "'gh' auth status"],
['-d', 'Ubuntu', '--', 'sh', '-c', expect.stringMatching(/gh[\s\S]*auth status/)],
{ encoding: 'utf-8', timeout: 5000 }
)
})
@@ -318,10 +318,18 @@ describe('preflight', () => {
if (command === 'gh' || command === 'glab') {
return Promise.reject(Object.assign(new Error('spawn ENOENT'), { code: 'ENOENT' }))
}
if (command === 'wsl.exe' && Array.isArray(args) && args.at(-1) === "'gh' --version") {
if (
command === 'wsl.exe' &&
Array.isArray(args) &&
String(args.at(-1)).includes("'gh' --version")
) {
return new Promise(() => {})
}
if (command === 'wsl.exe' && Array.isArray(args) && args.at(-1) === "'glab' --version") {
if (
command === 'wsl.exe' &&
Array.isArray(args) &&
String(args.at(-1)).includes("'glab' --version")
) {
return Promise.reject(Object.assign(new Error('spawn ENOENT'), { code: 'ENOENT' }))
}
throw new Error(`unexpected command ${String(command)}`)
@@ -606,9 +614,9 @@ describe('preflight', () => {
expect.arrayContaining([
'-d',
'Ubuntu',
'--exec',
'bash',
'-ic',
'--',
'sh',
'-c',
expect.stringContaining("'claude'")
]),
{ encoding: 'utf-8', timeout: 10000 }
@@ -637,7 +645,7 @@ describe('preflight', () => {
expect(resolveCliCommandsMock).not.toHaveBeenCalled()
expect(execFileAsyncMock).toHaveBeenCalledWith(
'wsl.exe',
expect.arrayContaining(['--exec', 'bash', '-ic', expect.stringContaining("'codex'")]),
expect.arrayContaining(['--', 'sh', '-c', expect.stringContaining("'codex'")]),
{ encoding: 'utf-8', timeout: 10000 }
)
})
+2 -5
View File
@@ -11,6 +11,7 @@ import { getGiteaAuthStatus } from '../gitea/client'
import { _resetKnownHostsCache } from '../gitlab/gl-utils'
import { getActiveMultiplexer } from './ssh'
import { detectWslCommandsOnPath, type WslPreflightTarget } from './preflight-wsl-agent-detection'
import { runPreflightCommandInWsl } from './preflight-wsl-command'
import { detectCommandsInInstallDirs } from './local-agent-install-dir-detection'
const execFileAsync = promisify(execFile)
const PREFLIGHT_COMMAND_TIMEOUT_MS = 5000
@@ -102,11 +103,7 @@ async function execCommandInWsl(
target: WslPreflightTarget,
command: string
): Promise<{ stdout: string; stderr: string }> {
const distroArgs = target.distro ? ['-d', target.distro] : []
const commandPromise = execFileAsync('wsl.exe', [...distroArgs, '--', 'bash', '-lc', command], {
encoding: 'utf-8',
timeout: PREFLIGHT_COMMAND_TIMEOUT_MS
}) as Promise<{ stdout: string; stderr: string }>
const commandPromise = runPreflightCommandInWsl(target, command, PREFLIGHT_COMMAND_TIMEOUT_MS)
return withPreflightTimeout('wsl.exe', commandPromise)
}
+4 -10
View File
@@ -345,10 +345,11 @@ describe('LocalPtyProvider', () => {
'-d',
'Debian',
'--',
'bash',
'sh',
'-c',
'cd \'/mnt/c/Users/jin/repo\' && export PATH="$HOME/.local/bin:$PATH" && exec bash -l'
expect.stringContaining("cd '/mnt/c/Users/jin/repo'")
])
expect(spawnCall[1][5]).toContain('exec "\\$_orca_wsl_shell" -l')
})
it('marks Orca terminal handle for WSL import when buildSpawnEnv opts in', async () => {
@@ -517,14 +518,7 @@ describe('LocalPtyProvider', () => {
expect(spawnMock).toHaveBeenCalledWith(
'wsl.exe',
[
'-d',
'Ubuntu',
'--',
'bash',
'-c',
'cd \'/home/jin/repo/subdir\' && export PATH="$HOME/.local/bin:$PATH" && exec bash -l'
],
['-d', 'Ubuntu', '--', 'sh', '-c', expect.stringContaining("cd '/home/jin/repo/subdir'")],
expect.objectContaining({ cwd: expect.any(String) })
)
})
+19 -29
View File
@@ -3,8 +3,18 @@ import {
encodePowerShellCommand,
getPowerShellOsc133Bootstrap
} from '../powershell-osc133-bootstrap'
import {
buildWslInteractiveLoginShellCommand,
escapeWslShCommandForWindows
} from '../../shared/wsl-login-shell-command'
import { resolveWindowsShellLaunchArgs } from './windows-shell-args'
function expectedWslArgs(linuxCwd: string, distro?: string): string[] {
const command = `cd '${linuxCwd}' && export PATH="$HOME/.local/bin:$PATH" && ${buildWslInteractiveLoginShellCommand()}`
const shellArgs = ['--', 'sh', '-c', escapeWslShCommandForWindows(command)]
return distro ? ['-d', distro, ...shellArgs] : shellArgs
}
describe('resolveWindowsShellLaunchArgs', () => {
it('returns cmd.exe args with chcp 65001 for UTF-8 output', () => {
const result = resolveWindowsShellLaunchArgs('cmd.exe', 'C:\\Users\\alice', 'C:\\Users\\alice')
@@ -95,12 +105,7 @@ describe('resolveWindowsShellLaunchArgs', () => {
'C:\\Users\\alice\\code',
'C:\\Users\\alice'
)
expect(result.shellArgs).toEqual([
'--',
'bash',
'-c',
'cd \'/mnt/c/Users/alice/code\' && export PATH="$HOME/.local/bin:$PATH" && exec bash -l'
])
expect(result.shellArgs).toEqual(expectedWslArgs('/mnt/c/Users/alice/code'))
// Why: WSL cannot cd into a Windows path, so node-pty must start from the
// user's Windows home and we inject the Linux cd into the shellArgs above.
expect(result.effectiveCwd).toBe('C:\\Users\\alice')
@@ -109,17 +114,16 @@ describe('resolveWindowsShellLaunchArgs', () => {
it('escapes single quotes when translating a WSL cwd', () => {
const result = resolveWindowsShellLaunchArgs('wsl.exe', "C:\\weird'path", 'C:\\Users\\alice')
// The injected bash cmd must not break out of the surrounding single
// quotes when the path contains a ' character.
expect(result.shellArgs[3]).toBe(
"cd '/mnt/c/weird'\\''path' && export PATH=\"$HOME/.local/bin:$PATH\" && exec bash -l"
)
// The injected sh cmd must not break out of the surrounding single quotes
// when the path contains a ' character.
expect(result.shellArgs[3]).toContain("cd '/mnt/c/weird'\\''path'")
expect(result.shellArgs[3]).toContain('exec "\\$_orca_wsl_shell" -l')
})
it('falls back to /mnt/c when cwd is not a drive-letter path', () => {
const result = resolveWindowsShellLaunchArgs('wsl.exe', '\\\\server\\share', 'C:\\Users\\alice')
expect(result.shellArgs[3]).toBe(
'cd \'/mnt/c\' && export PATH="$HOME/.local/bin:$PATH" && exec bash -l'
expect(result.shellArgs[3]).toContain(
'cd \'/mnt/c\' && export PATH="\\$HOME/.local/bin:\\$PATH"'
)
})
@@ -136,14 +140,7 @@ describe('resolveWindowsShellLaunchArgs', () => {
'\\\\wsl.localhost\\Ubuntu\\home\\alice\\repo',
'C:\\Users\\alice'
)
expect(result.shellArgs).toEqual([
'-d',
'Ubuntu',
'--',
'bash',
'-c',
'cd \'/home/alice/repo\' && export PATH="$HOME/.local/bin:$PATH" && exec bash -l'
])
expect(result.shellArgs).toEqual(expectedWslArgs('/home/alice/repo', 'Ubuntu'))
expect(result.effectiveCwd).toBe('C:\\Users\\alice')
expect(result.validationCwd).toBe('\\\\wsl.localhost\\Ubuntu\\home\\alice\\repo')
} finally {
@@ -162,14 +159,7 @@ describe('resolveWindowsShellLaunchArgs', () => {
{ distro: 'Ubuntu', treatPosixCwdAsWsl: true }
)
expect(result.shellArgs).toEqual([
'-d',
'Ubuntu',
'--',
'bash',
'-c',
'cd \'/home/alice/repo/subdir\' && export PATH="$HOME/.local/bin:$PATH" && exec bash -l'
])
expect(result.shellArgs).toEqual(expectedWslArgs('/home/alice/repo/subdir', 'Ubuntu'))
expect(result.effectiveCwd).toBe('C:\\Users\\alice')
expect(result.validationCwd).toBe('\\\\wsl.localhost\\Ubuntu\\home\\alice\\repo\\subdir')
})
+15 -11
View File
@@ -1,6 +1,11 @@
import { win32 as pathWin32 } from 'path'
import { isWindowsGitBashShellPath } from '../git-bash'
import { parseWslPath, toLinuxPath, toWindowsWslPath } from '../wsl'
import {
buildWslInteractiveLoginShellCommand,
escapeWslShCommandForWindows,
quotePosixShell
} from '../../shared/wsl-login-shell-command'
import {
encodePowerShellCommand,
getPowerShellOsc133Bootstrap
@@ -32,15 +37,14 @@ export type WindowsShellWslContext = {
}
function buildWslShellArgs(linuxCwd: string, distro?: string): string[] {
const escapedLinuxCwd = linuxCwd.replace(/'/g, "'\\''")
// Why: Orca's WSL bridge is installed under ~/.local/bin, but distro login
// files do not consistently include that directory before agent commands run.
const shellArgs = [
'--',
'bash',
'-c',
`cd '${escapedLinuxCwd}' && export PATH="$HOME/.local/bin:$PATH" && exec bash -l`
]
const setupCommand = [
`cd ${quotePosixShell(linuxCwd)}`,
'export PATH="$HOME/.local/bin:$PATH"',
buildWslInteractiveLoginShellCommand()
].join(' && ')
// Why: WSL users often customize zsh rather than bash; launch the distro's
// login shell so terminal PATH matches the environment Orca detects.
const shellArgs = ['--', 'sh', '-c', escapeWslShCommandForWindows(setupCommand)]
return distro ? ['-d', distro, ...shellArgs] : shellArgs
}
@@ -50,8 +54,8 @@ function buildWslShellArgs(linuxCwd: string, distro?: string): string[] {
* - powershell.exe / pwsh.exe: dot-source $PROFILE and force UTF-8 I/O so
* oh-my-posh / starship / PSReadLine keep working. `-NoExit` alone would
* skip the profile.
* - wsl.exe: translate the Windows cwd to /mnt/<drive>/... and enter a login
* bash inside the default distro.
* - wsl.exe: translate the Windows cwd to /mnt/<drive>/... and enter the
* distro user's login shell.
* - anything else: no args, same cwd. */
export function resolveWindowsShellLaunchArgs(
shellPath: string,
+456 -453
View File
@@ -1776,42 +1776,42 @@ function App(): React.JSX.Element {
<ConfirmationDialogProvider>
<LinkRoutingPreferenceDialogProvider>
<WorkspacePortScanner enabled={workspaceSessionReady} />
{/* Why: leaf-mounted retention sync keeps agent-status retention
{/* Why: leaf-mounted retention sync keeps agent-status retention
subscriptions from re-rendering the App tree. */}
<RetainedAgentsSyncGate />
{/* Why: workspace activation is a hot path; including activeWorktreeId
<RetainedAgentsSyncGate />
{/* Why: workspace activation is a hot path; including activeWorktreeId
in reset keys remounts whole surfaces during wake. */}
<RecoverableRenderErrorBoundary
boundaryId="app.workspace-shell"
surface="workspace-shell"
resetKey={activeView}
title={translate('auto.App.df1d56bf87', 'The workspace shell hit an error.')}
description={translate(
'auto.App.8504ddf267',
'The app is still running. Retry the shell or use the menu to report the crash details.'
)}
>
<div className="flex flex-row flex-1 min-h-0 overflow-hidden">
{/* Why: the non-workspace titlebar lives inside this left+center
<RecoverableRenderErrorBoundary
boundaryId="app.workspace-shell"
surface="workspace-shell"
resetKey={activeView}
title={translate('auto.App.df1d56bf87', 'The workspace shell hit an error.')}
description={translate(
'auto.App.8504ddf267',
'The app is still running. Retry the shell or use the menu to report the crash details.'
)}
>
<div className="flex flex-row flex-1 min-h-0 overflow-hidden">
{/* Why: the non-workspace titlebar lives inside this left+center
wrapper so it does not span over the right-sidebar column —
when the right sidebar is open, its own header anchors at the
top alongside the titlebar instead of being pushed below it. */}
<div className="flex flex-col flex-1 min-w-0 min-h-0">
{/* Why: in workspace view (split groups always enabled), the
<div className="flex flex-col flex-1 min-w-0 min-h-0">
{/* Why: in workspace view (split groups always enabled), the
full-width titlebar is removed so tab groups + terminal extend
to the top of the window. Left titlebar controls move to a
header above the sidebar. Settings, landing, and the tasks
page keep the titlebar. */}
{!workspaceActive && !stackedSidebarOpen ? (
<div className="titlebar">
<div className="flex items-center shrink-0 mr-2">{titlebarLeftControls}</div>
{titlebarMainStrip}
</div>
) : null}
<div className="flex flex-row flex-1 min-h-0 overflow-hidden">
{showSidebar ? (
workspaceActive || stackedSidebarOpen ? (
/* Why: left column wraps the sidebar with a titlebar-height
{!workspaceActive && !stackedSidebarOpen ? (
<div className="titlebar">
<div className="flex items-center shrink-0 mr-2">{titlebarLeftControls}</div>
{titlebarMainStrip}
</div>
) : null}
<div className="flex flex-row flex-1 min-h-0 overflow-hidden">
{showSidebar ? (
workspaceActive || stackedSidebarOpen ? (
/* Why: left column wraps the sidebar with a titlebar-height
header above it. The header holds the same controls
(traffic lights, sidebar toggle, "Orca" title, agent badge)
that the full-width titlebar held while the center and right
@@ -1819,476 +1819,479 @@ function App(): React.JSX.Element {
When the sidebar is collapsed, take this header out of flex
layout so the terminal/editor reclaim the left edge instead of
leaving behind a content-width blank strip. */
<div
className={`flex min-h-0 flex-col shrink-0${sidebarOpen ? '' : ' relative w-0 overflow-visible'}`}
>
<div
// Why: when the sidebar is collapsed, titlebar-left floats
// absolutely on top of the center column's own `border-l`
// (see TabGroupSplitLayout), occluding that seam. Add a
// `border-r` in the floating state so the vertical line
// between the traffic-light/nav cluster and the tab strip
// stays visible in both states. w-max keeps the floating
// header sized to its own controls instead of the w-0
// sidebar wrapper.
className={`titlebar-left${
sidebarOpen
? ''
: ' titlebar-left-floating absolute top-0 left-0 z-10 w-max border-r border-border'
}`}
style={{
// Why: the Sidebar resize hook updates the sidebar DOM width
// directly during drag and only persists to Zustand on
// mouseup. In workspace view, size this header from the
// wrapper's live width so it tracks those in-flight resizes
// instead of leaving a stale-width gap until the drag ends.
width: sidebarOpen ? '100%' : undefined
}}
className={`flex min-h-0 flex-col shrink-0${sidebarOpen ? '' : ' relative w-0 overflow-visible'}`}
>
{titlebarLeftControls}
</div>
<div className="flex min-h-0 flex-1">
{/* Why: the workspace-view wrapper adds a fixed 36px header
<div
// Why: when the sidebar is collapsed, titlebar-left floats
// absolutely on top of the center column's own `border-l`
// (see TabGroupSplitLayout), occluding that seam. Add a
// `border-r` in the floating state so the vertical line
// between the traffic-light/nav cluster and the tab strip
// stays visible in both states. w-max keeps the floating
// header sized to its own controls instead of the w-0
// sidebar wrapper.
className={`titlebar-left${
sidebarOpen
? ''
: ' titlebar-left-floating absolute top-0 left-0 z-10 w-max border-r border-border'
}`}
style={{
// Why: the Sidebar resize hook updates the sidebar DOM width
// directly during drag and only persists to Zustand on
// mouseup. In workspace view, size this header from the
// wrapper's live width so it tracks those in-flight resizes
// instead of leaving a stale-width gap until the drag ends.
width: sidebarOpen ? '100%' : undefined
}}
>
{titlebarLeftControls}
</div>
<div className="flex min-h-0 flex-1">
{/* Why: the workspace-view wrapper adds a fixed 36px header
above the sidebar. Without a flex-1/min-h-0 slot here,
the sidebar falls back to its content height, so the
worktree list loses its scroll viewport and the fixed
bottom toolbar (including Add Project) gets pushed offscreen. */}
<RecoverableRenderErrorBoundary
boundaryId="sidebar.worktrees"
surface="sidebar"
resetKey={activeView}
title={translate(
'auto.App.1468601e7b',
'The workspace list hit an error.'
)}
description={translate(
'auto.App.bdc71dddc9',
'The active workspace remains open. Retry the list or switch views.'
)}
>
<Sidebar
worktreeScrollOffsetRef={worktreeSidebarScrollOffsetRef}
worktreeScrollAnchorRef={worktreeSidebarScrollAnchorRef}
/>
</RecoverableRenderErrorBoundary>
<RecoverableRenderErrorBoundary
boundaryId="sidebar.worktrees"
surface="sidebar"
resetKey={activeView}
title={translate(
'auto.App.1468601e7b',
'The workspace list hit an error.'
)}
description={translate(
'auto.App.bdc71dddc9',
'The active workspace remains open. Retry the list or switch views.'
)}
>
<Sidebar
worktreeScrollOffsetRef={worktreeSidebarScrollOffsetRef}
worktreeScrollAnchorRef={worktreeSidebarScrollAnchorRef}
/>
</RecoverableRenderErrorBoundary>
</div>
</div>
</div>
) : (
<RecoverableRenderErrorBoundary
boundaryId="sidebar.worktrees"
surface="sidebar"
resetKey={activeView}
title={translate('auto.App.1468601e7b', 'The workspace list hit an error.')}
description={translate(
'auto.App.cba0fafda5',
'The active page remains open. Retry the list or switch views.'
)}
>
<Sidebar
worktreeScrollOffsetRef={worktreeSidebarScrollOffsetRef}
worktreeScrollAnchorRef={worktreeSidebarScrollAnchorRef}
/>
</RecoverableRenderErrorBoundary>
)
) : null}
<div className="flex flex-col flex-1 min-w-0 min-h-0 overflow-hidden">
{stackedSidebarOpen ? (
<div className="titlebar">{titlebarMainStrip}</div>
) : (
<RecoverableRenderErrorBoundary
boundaryId="sidebar.worktrees"
surface="sidebar"
resetKey={activeView}
title={translate(
'auto.App.1468601e7b',
'The workspace list hit an error.'
)}
description={translate(
'auto.App.cba0fafda5',
'The active page remains open. Retry the list or switch views.'
)}
>
<Sidebar
worktreeScrollOffsetRef={worktreeSidebarScrollOffsetRef}
worktreeScrollAnchorRef={worktreeSidebarScrollAnchorRef}
/>
</RecoverableRenderErrorBoundary>
)
) : null}
<div className="relative flex flex-1 min-w-0 min-h-0 overflow-hidden">
{/* Why: right sidebar toggle floats at the top-right of the center
<div className="flex flex-col flex-1 min-w-0 min-h-0 overflow-hidden">
{stackedSidebarOpen ? (
<div className="titlebar">{titlebarMainStrip}</div>
) : null}
<div className="relative flex flex-1 min-w-0 min-h-0 overflow-hidden">
{/* Why: right sidebar toggle floats at the top-right of the center
column so it's always accessible whether the right sidebar is
open or closed. Match the RightSidebar header's 36px height and
top-0 anchor so the icon's vertical center is identical between
open and closed states — otherwise toggling makes the icon jump
a few pixels, which reads as layout jitter. */}
{workspaceActive && !rightSidebarOpen && (
<div
className="absolute top-0 z-10 flex items-center h-[36px]"
style={
{
// Why: right: var(--window-controls-width) is the single
// mechanism that keeps the toggle clear of the
// fixed-position window-controls overlay on Windows (138px)
// and sits at the right edge on non-Windows (0px). No
// internal spacer needed — adding one would push the button
// a further 138px to the left and cover the pane-actions
// Ellipsis button with an un-clickable div.
right: 'var(--window-controls-width)',
WebkitAppRegion: 'no-drag'
} as React.CSSProperties
}
>
{rightSidebarToggle}
</div>
)}
<div className="flex flex-1 min-w-0 min-h-0 flex-col">
{shouldMountTerminalWorkbench ? (
{workspaceActive && !rightSidebarOpen && (
<div
className={
activeView !== 'terminal' ||
!activeWorktreeId ||
activeCreationLoaderVisible
? 'hidden flex-1 min-w-0 min-h-0'
: 'flex flex-1 min-w-0 min-h-0'
className="absolute top-0 z-10 flex items-center h-[36px]"
style={
{
// Why: right: var(--window-controls-width) is the single
// mechanism that keeps the toggle clear of the
// fixed-position window-controls overlay on Windows (138px)
// and sits at the right edge on non-Windows (0px). No
// internal spacer needed — adding one would push the button
// a further 138px to the left and cover the pane-actions
// Ellipsis button with an un-clickable div.
right: 'var(--window-controls-width)',
WebkitAppRegion: 'no-drag'
} as React.CSSProperties
}
>
<Suspense fallback={null}>
<RecoverableRenderErrorBoundary
boundaryId="terminal.workbench"
surface="terminal-workbench"
resetKey="terminal"
title={translate(
'auto.App.5a9519aef0',
'The workspace workbench hit an error.'
)}
description={translate(
'auto.App.98d4ea2823',
'Terminal, browser, or editor rendering failed in this workspace. Retry to remount it.'
)}
>
<Terminal />
</RecoverableRenderErrorBoundary>
</Suspense>
{rightSidebarToggle}
</div>
)}
<div className="flex flex-1 min-w-0 min-h-0 flex-col">
{shouldMountTerminalWorkbench ? (
<div
className={
activeView !== 'terminal' ||
!activeWorktreeId ||
activeCreationLoaderVisible
? 'hidden flex-1 min-w-0 min-h-0'
: 'flex flex-1 min-w-0 min-h-0'
}
>
<Suspense fallback={null}>
<RecoverableRenderErrorBoundary
boundaryId="terminal.workbench"
surface="terminal-workbench"
resetKey="terminal"
title={translate(
'auto.App.5a9519aef0',
'The workspace workbench hit an error.'
)}
description={translate(
'auto.App.98d4ea2823',
'Terminal, browser, or editor rendering failed in this workspace. Retry to remount it.'
)}
>
<Terminal />
</RecoverableRenderErrorBoundary>
</Suspense>
</div>
) : null}
<Suspense fallback={null}>
<RecoverableRenderErrorBoundary
boundaryId={`page.${activeView}`}
surface="page"
resetKey={activeView}
title={translate('auto.App.b7a714db1e', 'This page hit an error.')}
description={translate(
'auto.App.03a14f6b5b',
'Retry the page or navigate to another Orca surface.'
)}
>
{activeView === 'settings' ? <Settings /> : null}
{activeView === 'skills' ? <SkillsPage /> : null}
{activeView === 'tasks' ? <TaskPage /> : null}
{activeView === 'automations' ? <AutomationsPage /> : null}
{activeView === 'activity' ? <ActivityPrototypePage /> : null}
{activeView === 'space' ? <WorkspaceSpacePage /> : null}
{activeView === 'mobile' ? <MobilePage /> : null}
{activeView === 'terminal' &&
activeCreationLoaderVisible &&
activePendingCreationId ? (
<WorktreeCreationPanel creationId={activePendingCreationId} />
) : null}
{activeView === 'terminal' &&
!activeWorktreeId &&
!activeCreationLoaderVisible ? (
<Landing />
) : null}
</RecoverableRenderErrorBoundary>
</Suspense>
</div>
{showFloatingTerminalButton ? (
<FloatingTerminalToggleButton
open={floatingTerminalOpen}
onToggle={() => setFloatingTerminalOpenWithFocus((open) => !open)}
/>
) : null}
<Suspense fallback={null}>
<RecoverableRenderErrorBoundary
boundaryId={`page.${activeView}`}
surface="page"
resetKey={activeView}
title={translate('auto.App.b7a714db1e', 'This page hit an error.')}
description={translate(
'auto.App.03a14f6b5b',
'Retry the page or navigate to another Orca surface.'
)}
>
{activeView === 'settings' ? <Settings /> : null}
{activeView === 'skills' ? <SkillsPage /> : null}
{activeView === 'tasks' ? <TaskPage /> : null}
{activeView === 'automations' ? <AutomationsPage /> : null}
{activeView === 'activity' ? <ActivityPrototypePage /> : null}
{activeView === 'space' ? <WorkspaceSpacePage /> : null}
{activeView === 'mobile' ? <MobilePage /> : null}
{activeView === 'terminal' &&
activeCreationLoaderVisible &&
activePendingCreationId ? (
<WorktreeCreationPanel creationId={activePendingCreationId} />
) : null}
{activeView === 'terminal' &&
!activeWorktreeId &&
!activeCreationLoaderVisible ? (
<Landing />
) : null}
</RecoverableRenderErrorBoundary>
</Suspense>
</div>
{showFloatingTerminalButton ? (
<FloatingTerminalToggleButton
open={floatingTerminalOpen}
onToggle={() => setFloatingTerminalOpenWithFocus((open) => !open)}
/>
) : null}
</div>
</div>
</div>
</div>
{/* Why: keep the right-sidebar shell mounted for layout stability.
{/* Why: keep the right-sidebar shell mounted for layout stability.
Its heavy panels disconnect while closed so workspace wake stays
responsive. Unmount on the tasks view since that surface is
intentionally distraction-free. */}
{showRightSidebarControls ? (
{showRightSidebarControls ? (
<RecoverableRenderErrorBoundary
boundaryId="right-sidebar"
surface="right-sidebar"
resetKey={
rightSidebarTab === 'explorer'
? `${rightSidebarTab}:${rightSidebarExplorerView}`
: rightSidebarTab
}
title={translate('auto.App.ed6b168d00', 'The right sidebar hit an error.')}
description={translate(
'auto.App.8d1e160ed1',
'Retry the sidebar or switch tabs to reload this surface.'
)}
>
<RightSidebar />
</RecoverableRenderErrorBoundary>
) : null}
</div>
</RecoverableRenderErrorBoundary>
{shouldMountFloatingTerminalPanel ? (
<Suspense fallback={null}>
<RecoverableRenderErrorBoundary
boundaryId="right-sidebar"
surface="right-sidebar"
resetKey={
rightSidebarTab === 'explorer'
? `${rightSidebarTab}:${rightSidebarExplorerView}`
: rightSidebarTab
}
title={translate('auto.App.ed6b168d00', 'The right sidebar hit an error.')}
boundaryId="overlay.floating-workspace"
surface="overlay"
resetKey={floatingTerminalOpen}
compact
title={translate('auto.App.1b3024bcd6', 'The floating workspace hit an error.')}
description={translate(
'auto.App.8d1e160ed1',
'Retry the sidebar or switch tabs to reload this surface.'
'auto.App.7cbfbf622f',
'Retry the floating workspace or close and reopen it.'
)}
>
<RightSidebar />
<FloatingTerminalPanel
open={floatingTerminalOpen}
onOpenChange={setFloatingTerminalOpenWithFocus}
tourInteractionSnapshot={floatingWorkspaceTourInteractionSnapshotRef.current}
/>
</RecoverableRenderErrorBoundary>
</Suspense>
) : null}
{statusBarVisible ? (
<Suspense
fallback={
<div className="h-6 min-h-[24px] shrink-0 border-t border-border bg-[var(--bg-titlebar,var(--card))]" />
}
>
<RecoverableRenderErrorBoundary
boundaryId="overlay.status-bar"
surface="overlay"
resetKey={activeView}
compact
title={translate('auto.App.2e8ff36f94', 'The status bar hit an error.')}
description={translate(
'auto.App.8a023cea1f',
'Retry the status bar to remount its controls.'
)}
>
<StatusBar floatingTerminalOpen={floatingTerminalOpen} />
</RecoverableRenderErrorBoundary>
</Suspense>
) : null}
{/* Why: root overlays can render Radix <Tooltip>s; keep them inside
the shared provider so lazy surfaces mount safely from any entry point. */}
<Suspense fallback={null}>
{resolvedMountedLazyModalIds.has('new-workspace-composer') ? (
<RecoverableRenderErrorBoundary
boundaryId="modal.new-workspace-composer"
surface="modal"
resetKey={activeModal === 'new-workspace-composer'}
compact
>
<NewWorkspaceComposerModal />
</RecoverableRenderErrorBoundary>
) : null}
</div>
</RecoverableRenderErrorBoundary>
{shouldMountFloatingTerminalPanel ? (
{resolvedMountedLazyModalIds.has('workspace-cleanup') ? (
<RecoverableRenderErrorBoundary
boundaryId="modal.workspace-cleanup"
surface="modal"
resetKey={activeModal === 'workspace-cleanup'}
compact
>
<WorkspaceCleanupDialog />
</RecoverableRenderErrorBoundary>
) : null}
</Suspense>
<Suspense fallback={null}>
<RecoverableRenderErrorBoundary
boundaryId="overlay.floating-workspace"
surface="overlay"
resetKey={floatingTerminalOpen}
compact
title={translate('auto.App.1b3024bcd6', 'The floating workspace hit an error.')}
description={translate(
'auto.App.7cbfbf622f',
'Retry the floating workspace or close and reopen it.'
)}
>
<FloatingTerminalPanel
open={floatingTerminalOpen}
onOpenChange={setFloatingTerminalOpenWithFocus}
tourInteractionSnapshot={floatingWorkspaceTourInteractionSnapshotRef.current}
/>
</RecoverableRenderErrorBoundary>
{resolvedMountedLazyModalIds.has('quick-open') ? (
<RecoverableRenderErrorBoundary
boundaryId="modal.quick-open"
surface="modal"
resetKey={activeModal === 'quick-open'}
compact
>
<QuickOpen />
</RecoverableRenderErrorBoundary>
) : null}
{resolvedMountedLazyModalIds.has('worktree-palette') ? (
<RecoverableRenderErrorBoundary
boundaryId="modal.worktree-palette"
surface="modal"
resetKey={activeModal === 'worktree-palette'}
compact
>
<WorktreeJumpPalette />
</RecoverableRenderErrorBoundary>
) : null}
{resolvedMountedLazyModalIds.has('setup-guide') ? (
<RecoverableRenderErrorBoundary
boundaryId="modal.setup-guide"
surface="modal"
resetKey={activeModal === 'setup-guide'}
compact
>
<SetupGuideModal />
</RecoverableRenderErrorBoundary>
) : null}
{resolvedMountedLazyModalIds.has('feature-wall') ? (
<RecoverableRenderErrorBoundary
boundaryId="modal.feature-wall"
surface="modal"
resetKey={activeModal === 'feature-wall'}
compact
>
<FeatureWallModal />
</RecoverableRenderErrorBoundary>
) : null}
{resolvedMountedLazyModalIds.has('feature-tips') ? (
<RecoverableRenderErrorBoundary
boundaryId="modal.feature-tips"
surface="modal"
resetKey={activeModal === 'feature-tips'}
compact
>
<FeatureTipsModal />
</RecoverableRenderErrorBoundary>
) : null}
</Suspense>
) : null}
{statusBarVisible ? (
<Suspense
fallback={
<div className="h-6 min-h-[24px] shrink-0 border-t border-border bg-[var(--bg-titlebar,var(--card))]" />
}
>
<RecoverableRenderErrorBoundary
boundaryId="overlay.status-bar"
surface="overlay"
resetKey={activeView}
compact
title={translate('auto.App.2e8ff36f94', 'The status bar hit an error.')}
description={translate(
'auto.App.8a023cea1f',
'Retry the status bar to remount its controls.'
)}
>
<StatusBar floatingTerminalOpen={floatingTerminalOpen} />
</RecoverableRenderErrorBoundary>
</Suspense>
) : null}
{/* Why: root overlays can render Radix <Tooltip>s; keep them inside
the shared provider so lazy surfaces mount safely from any entry point. */}
<Suspense fallback={null}>
{resolvedMountedLazyModalIds.has('new-workspace-composer') ? (
<RecoverableRenderErrorBoundary
boundaryId="modal.new-workspace-composer"
surface="modal"
resetKey={activeModal === 'new-workspace-composer'}
compact
>
<NewWorkspaceComposerModal />
</RecoverableRenderErrorBoundary>
{shouldMountSetupGuideTelemetryObserver ? (
<Suspense fallback={null}>
<SetupGuideTelemetryObserver />
</Suspense>
) : null}
{resolvedMountedLazyModalIds.has('workspace-cleanup') ? (
<RecoverableRenderErrorBoundary
boundaryId="modal.workspace-cleanup"
surface="modal"
resetKey={activeModal === 'workspace-cleanup'}
compact
>
<WorkspaceCleanupDialog />
</RecoverableRenderErrorBoundary>
{shouldMountContextualTourOverlay ? (
<Suspense fallback={null}>
<ContextualTourOverlay />
</Suspense>
) : null}
</Suspense>
<Suspense fallback={null}>
{resolvedMountedLazyModalIds.has('quick-open') ? (
<RecoverableRenderErrorBoundary
boundaryId="modal.quick-open"
surface="modal"
resetKey={activeModal === 'quick-open'}
compact
>
<QuickOpen />
</RecoverableRenderErrorBoundary>
) : null}
{resolvedMountedLazyModalIds.has('worktree-palette') ? (
<RecoverableRenderErrorBoundary
boundaryId="modal.worktree-palette"
surface="modal"
resetKey={activeModal === 'worktree-palette'}
compact
>
<WorktreeJumpPalette />
</RecoverableRenderErrorBoundary>
) : null}
{resolvedMountedLazyModalIds.has('setup-guide') ? (
<RecoverableRenderErrorBoundary
boundaryId="modal.setup-guide"
surface="modal"
resetKey={activeModal === 'setup-guide'}
compact
>
<SetupGuideModal />
</RecoverableRenderErrorBoundary>
) : null}
{resolvedMountedLazyModalIds.has('feature-wall') ? (
<RecoverableRenderErrorBoundary
boundaryId="modal.feature-wall"
surface="modal"
resetKey={activeModal === 'feature-wall'}
compact
>
<FeatureWallModal />
</RecoverableRenderErrorBoundary>
) : null}
{resolvedMountedLazyModalIds.has('feature-tips') ? (
<RecoverableRenderErrorBoundary
boundaryId="modal.feature-tips"
surface="modal"
resetKey={activeModal === 'feature-tips'}
compact
>
<FeatureTipsModal />
</RecoverableRenderErrorBoundary>
) : null}
</Suspense>
{shouldMountSetupGuideTelemetryObserver ? (
<Suspense fallback={null}>
<SetupGuideTelemetryObserver />
</Suspense>
) : null}
{shouldMountContextualTourOverlay ? (
<Suspense fallback={null}>
<ContextualTourOverlay />
</Suspense>
) : null}
{/* Why: mount PetOverlay only after persisted UI hydration, with
{/* Why: mount PetOverlay only after persisted UI hydration, with
both independent pet toggles allowing it; otherwise a hidden pet
flashes while the store still has default visibility. */}
{renderPetOverlay ? (
<Suspense fallback={null}>
<RecoverableRenderErrorBoundary
boundaryId="overlay.pet"
surface="overlay"
resetKey={petVisible}
compact
>
<PetOverlay />
</RecoverableRenderErrorBoundary>
</Suspense>
) : null}
{shouldMountUpdateCard ? (
<Suspense fallback={null}>
<RecoverableRenderErrorBoundary
boundaryId="overlay.update-card"
surface="overlay"
resetKey={activeView}
compact
>
<UpdateCard />
</RecoverableRenderErrorBoundary>
</Suspense>
) : null}
<RecoverableRenderErrorBoundary
boundaryId="overlay.star-nag"
surface="overlay"
resetKey={activeView}
compact
>
<StarNagCard />
</RecoverableRenderErrorBoundary>
{/* Why: the existing-user opt-in banner mounts at App root so it
{renderPetOverlay ? (
<Suspense fallback={null}>
<RecoverableRenderErrorBoundary
boundaryId="overlay.pet"
surface="overlay"
resetKey={petVisible}
compact
>
<PetOverlay />
</RecoverableRenderErrorBoundary>
</Suspense>
) : null}
{shouldMountUpdateCard ? (
<Suspense fallback={null}>
<RecoverableRenderErrorBoundary
boundaryId="overlay.update-card"
surface="overlay"
resetKey={activeView}
compact
>
<UpdateCard />
</RecoverableRenderErrorBoundary>
</Suspense>
) : null}
<RecoverableRenderErrorBoundary
boundaryId="overlay.star-nag"
surface="overlay"
resetKey={activeView}
compact
>
<StarNagCard />
</RecoverableRenderErrorBoundary>
{/* Why: the existing-user opt-in banner mounts at App root so it
renders once per renderer session, not per view. It gates
internally on the cohort markers populated by the migration,
so it only shows for users who installed before the telemetry
release and have not yet resolved consent. New users get no
first-launch surface — see telemetry-plan.md §First-launch
experience. */}
<RecoverableRenderErrorBoundary
boundaryId="overlay.telemetry-first-launch"
surface="overlay"
resetKey={settings?.telemetry?.optedIn ?? 'unknown'}
compact
>
<TelemetryFirstLaunchSurface />
</RecoverableRenderErrorBoundary>
<RecoverableRenderErrorBoundary
boundaryId="overlay.zoom"
surface="overlay"
resetKey={activeView}
compact
>
<ZoomOverlay />
</RecoverableRenderErrorBoundary>
<Suspense fallback={null}>
{activeModal === 'delete-worktree' ? (
<RecoverableRenderErrorBoundary
boundaryId="modal.delete-worktree"
surface="modal"
resetKey
compact
>
<DeleteWorktreeDialog />
</RecoverableRenderErrorBoundary>
<RecoverableRenderErrorBoundary
boundaryId="overlay.telemetry-first-launch"
surface="overlay"
resetKey={settings?.telemetry?.optedIn ?? 'unknown'}
compact
>
<TelemetryFirstLaunchSurface />
</RecoverableRenderErrorBoundary>
<RecoverableRenderErrorBoundary
boundaryId="overlay.zoom"
surface="overlay"
resetKey={activeView}
compact
>
<ZoomOverlay />
</RecoverableRenderErrorBoundary>
<Suspense fallback={null}>
{activeModal === 'delete-worktree' ? (
<RecoverableRenderErrorBoundary
boundaryId="modal.delete-worktree"
surface="modal"
resetKey
compact
>
<DeleteWorktreeDialog />
</RecoverableRenderErrorBoundary>
) : null}
</Suspense>
{hasSshCredentialRequest ? (
<Suspense fallback={null}>
<RecoverableRenderErrorBoundary
boundaryId="modal.ssh-passphrase"
surface="modal"
resetKey={activeModal}
compact
>
<SshPassphraseDialog />
</RecoverableRenderErrorBoundary>
</Suspense>
) : null}
</Suspense>
{hasSshCredentialRequest ? (
<Suspense fallback={null}>
<RecoverableRenderErrorBoundary
boundaryId="modal.ssh-passphrase"
surface="modal"
resetKey={activeModal}
compact
>
<SshPassphraseDialog />
</RecoverableRenderErrorBoundary>
</Suspense>
) : null}
<RecoverableRenderErrorBoundary
boundaryId="modal.markdown-template-picker"
surface="modal"
resetKey={activeModal}
compact
>
<MarkdownTemplatePicker />
</RecoverableRenderErrorBoundary>
<RecoverableRenderErrorBoundary
boundaryId="modal.crash-report"
surface="modal"
reportAsCrash={false}
resetKey={activeModal}
compact
title={translate('auto.App.722d03aa62', 'The crash report dialog hit an error.')}
description={translate(
'auto.App.acd66311dc',
'Use the Help menu after retrying if you still need diagnostics.'
)}
>
<CrashReportDialog />
</RecoverableRenderErrorBoundary>
{onboarding && shouldRenderOnboarding && !onboardingSettingsDetourActive ? (
<Suspense fallback={null}>
<RecoverableRenderErrorBoundary
boundaryId="modal.onboarding"
surface="modal"
resetKey={onboardingSettingsDetourActive}
title={translate('auto.App.f02d37278a', 'Onboarding hit an error.')}
description={translate(
'auto.App.221a95ba38',
'Retry onboarding or close it and continue in the app.'
)}
>
<OnboardingFlow
onboarding={onboarding}
onOnboardingChange={setOnboarding}
onSettingsDetourStart={beginOnboardingSettingsDetour}
/>
</RecoverableRenderErrorBoundary>
</Suspense>
) : null}
{shouldMountDictationController ? (
<Suspense fallback={null}>
<RecoverableRenderErrorBoundary
boundaryId="overlay.dictation"
surface="overlay"
resetKey={activeView}
compact
>
<DictationController />
</RecoverableRenderErrorBoundary>
</Suspense>
) : null}
<RecoverableRenderErrorBoundary
boundaryId="overlay.recent-tab-switcher"
surface="overlay"
resetKey={activeView}
compact
>
<RecentTabSwitcher />
</RecoverableRenderErrorBoundary>
<RecoverableRenderErrorBoundary
boundaryId="modal.markdown-template-picker"
surface="modal"
resetKey={activeModal}
compact
>
<MarkdownTemplatePicker />
</RecoverableRenderErrorBoundary>
<RecoverableRenderErrorBoundary
boundaryId="modal.crash-report"
surface="modal"
reportAsCrash={false}
resetKey={activeModal}
compact
title={translate('auto.App.722d03aa62', 'The crash report dialog hit an error.')}
description={translate(
'auto.App.acd66311dc',
'Use the Help menu after retrying if you still need diagnostics.'
)}
>
<CrashReportDialog />
</RecoverableRenderErrorBoundary>
{onboarding && shouldRenderOnboarding && !onboardingSettingsDetourActive ? (
<Suspense fallback={null}>
<RecoverableRenderErrorBoundary
boundaryId="modal.onboarding"
surface="modal"
resetKey={onboardingSettingsDetourActive}
title={translate('auto.App.f02d37278a', 'Onboarding hit an error.')}
description={translate(
'auto.App.221a95ba38',
'Retry onboarding or close it and continue in the app.'
)}
>
<OnboardingFlow
onboarding={onboarding}
onOnboardingChange={setOnboarding}
onSettingsDetourStart={beginOnboardingSettingsDetour}
/>
</RecoverableRenderErrorBoundary>
</Suspense>
) : null}
{shouldMountDictationController ? (
<Suspense fallback={null}>
<RecoverableRenderErrorBoundary
boundaryId="overlay.dictation"
surface="overlay"
resetKey={activeView}
compact
>
<DictationController />
</RecoverableRenderErrorBoundary>
</Suspense>
) : null}
<RecoverableRenderErrorBoundary
boundaryId="overlay.recent-tab-switcher"
surface="overlay"
resetKey={activeView}
compact
>
<RecentTabSwitcher />
</RecoverableRenderErrorBoundary>
</LinkRoutingPreferenceDialogProvider>
</ConfirmationDialogProvider>
</TooltipProvider>
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest'
import {
buildSkillInstallCommandForRuntime,
getSkillDiscoveryTargetForRuntime
} from './CliSkillRuntimeSetup'
describe('CliSkillRuntimeSetup runtime helpers', () => {
it('wraps WSL skill installs in the selected distro login shell', () => {
const command = buildSkillInstallCommandForRuntime('npx skills add orchestration --global', {
runtime: 'wsl',
wslDistro: 'Ubuntu',
label: 'WSL Ubuntu'
})
expect(command).toContain("wsl.exe -d 'Ubuntu' -- sh -c")
expect(command).toContain('getent passwd')
expect(command).toContain('npx skills add orchestration --global')
})
it('preserves the selected WSL distro for skill discovery', () => {
expect(
getSkillDiscoveryTargetForRuntime({
runtime: 'wsl',
wslDistro: 'Ubuntu',
label: 'WSL Ubuntu'
})
).toEqual({ runtime: 'wsl', wslDistro: 'Ubuntu' })
})
})
@@ -1,4 +1,8 @@
import type { GlobalSettings } from '../../../../shared/types'
import {
buildWslLoginShellCommand,
escapeWslShCommandForWindows
} from '../../../../shared/wsl-login-shell-command'
import { toast } from 'sonner'
import type { CliInstallStatus } from '../../../../shared/cli-install-types'
import {
@@ -67,7 +71,16 @@ export function buildSkillInstallCommandForRuntime(
const distroArg = runtime.wslDistro?.trim()
? ` -d ${quotePowerShellSingle(runtime.wslDistro.trim())}`
: ''
return `wsl.exe${distroArg} -- bash -lc ${quotePowerShellSingle(command)}`
const wslCommand = escapeWslShCommandForWindows(buildWslLoginShellCommand(command))
return `wsl.exe${distroArg} -- sh -c ${quotePowerShellSingle(wslCommand)}`
}
export function getSkillDiscoveryTargetForRuntime(
runtime: LocalAgentRuntime
): { runtime: 'wsl'; wslDistro?: string | null } | undefined {
return runtime.runtime === 'wsl'
? { runtime: 'wsl', wslDistro: runtime.wslDistro ?? null }
: undefined
}
export function getAgentSkillTerminalShellOverride(
@@ -1,17 +1,11 @@
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
import {
Accessibility,
Camera,
ExternalLink,
MonitorCog,
RefreshCw,
ShieldCheck
} from 'lucide-react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { ExternalLink, MonitorCog, RefreshCw, ShieldCheck } from 'lucide-react'
import { toast } from 'sonner'
import type { SkillDiscoveryTarget } from '../../../../shared/skills'
import type { GlobalSettings } from '../../../../shared/types'
import type {
ComputerUsePermissionId,
ComputerUsePermissionState,
ComputerUsePermissionStatus
ComputerUsePermissionState
} from '../../../../shared/computer-use-permissions-types'
import {
COMPUTER_USE_SKILL_INSTALL_COMMAND,
@@ -19,7 +13,8 @@ import {
} from '@/lib/agent-feature-install-commands'
import {
AGENT_SKILL_CLI_PREREQUISITE_NOTICE,
ensureOrcaCliAvailableForAgentSkillTerminal
ensureOrcaCliAvailableForAgentSkillTerminal,
isOrcaCliAvailableOnPath
} from '@/lib/agent-skill-cli-prerequisite'
import {
GLOBAL_AGENT_SKILL_SOURCE_KINDS,
@@ -29,65 +24,32 @@ import { useAppStore } from '@/store'
import { Button } from '../ui/button'
import { Badge } from '../ui/badge'
import { AgentSkillSetupPanel } from './AgentSkillSetupPanel'
import {
buildSkillInstallCommandForRuntime,
ensureWslCliAvailableForAgentSkillTerminal,
getAgentSkillTerminalShellOverride,
getSkillDiscoveryTargetForRuntime
} from './CliSkillRuntimeSetup'
import { getDesktopPlatformFromUserAgent } from './GeneralPane'
import {
COMPUTER_USE_PERMISSIONS,
getComputerUsePermissionStatusClass,
getComputerUsePermissionStatusLabel
} from './computer-use-permission-definitions'
import { getComputerUseSkillRuntime } from './computer-use-skill-runtime'
import { getComputerUseSummary } from './computer-use-summary'
import { translate } from '@/i18n/i18n'
export { getComputerUsePaneSearchEntries } from './computer-use-search'
type PermissionDefinition = {
id: ComputerUsePermissionId
label: string
description: string
icon: ReactNode
type ComputerUsePaneProps = {
currentPlatform?: string
settings?: GlobalSettings | null
wslSupportedPlatform?: boolean
wslAvailable?: boolean
wslCapabilitiesLoading?: boolean
}
const PERMISSIONS: PermissionDefinition[] = [
{
id: 'accessibility',
get label() {
return translate('auto.components.settings.ComputerUsePane.6b5a2cd3a5', 'Accessibility')
},
get description() {
return translate(
'auto.components.settings.ComputerUsePane.4d03dec2d0',
'Read app interface trees and perform requested actions.'
)
},
icon: <Accessibility className="size-4" />
},
{
id: 'screenshots',
get label() {
return translate('auto.components.settings.ComputerUsePane.07bbe4c4cb', 'Screenshots')
},
get description() {
return translate(
'auto.components.settings.ComputerUsePane.0c9a33f468',
'Capture app windows so agents can inspect visual state.'
)
},
icon: <Camera className="size-4" />
}
]
function statusLabel(status: ComputerUsePermissionStatus | undefined): string {
switch (status) {
case 'granted':
return 'Granted'
case 'unsupported':
return 'macOS only'
case 'not-granted':
case undefined:
return 'Not enabled'
}
}
function statusClass(status: ComputerUsePermissionStatus | undefined): string {
if (status === 'granted') {
return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
}
return 'border-border bg-muted text-muted-foreground'
}
export function ComputerUsePane(): React.JSX.Element {
export function ComputerUsePane(props: ComputerUsePaneProps = {}): React.JSX.Element {
const [platform, setPlatform] = useState<NodeJS.Platform | null>(null)
const [states, setStates] = useState<ComputerUsePermissionState[]>([])
const [loading, setLoading] = useState(true)
@@ -98,12 +60,35 @@ export function ComputerUsePane(): React.JSX.Element {
const permissionOperationSequence = useRef(0)
const mountedRef = useRef(true)
const [helperUnavailableReason, setHelperUnavailableReason] = useState<string | null>(null)
const currentPlatform =
props.currentPlatform ??
(typeof navigator === 'undefined'
? 'other'
: getDesktopPlatformFromUserAgent(navigator.userAgent))
const skillRuntime = useMemo(() => getComputerUseSkillRuntime(props), [props])
const skillDiscoveryTarget = useMemo<SkillDiscoveryTarget | undefined>(
() => getSkillDiscoveryTargetForRuntime(skillRuntime),
[skillRuntime]
)
const skillInstallCommand = buildSkillInstallCommandForRuntime(
COMPUTER_USE_SKILL_INSTALL_COMMAND,
skillRuntime
)
const skillTerminalShellOverride = props.settings
? getAgentSkillTerminalShellOverride(currentPlatform, props.settings, skillRuntime)
: undefined
const getSkillPrerequisiteStatus = () =>
skillRuntime.runtime === 'wsl'
? window.api.cli.getWslInstallStatus()
: window.api.cli.getInstallStatus()
const {
installed: computerUseSkillDetected,
loading: computerUseSkillLoading,
error: computerUseSkillError,
refresh: refreshComputerUseSkill
} = useInstalledAgentSkill(COMPUTER_USE_SKILL_NAME, {
discoveryTarget: skillDiscoveryTarget,
sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS
})
@@ -111,30 +96,21 @@ export function ComputerUsePane(): React.JSX.Element {
() => new Map(states.map((state) => [state.id, state.status] as const)),
[states]
)
const grantedCount = PERMISSIONS.filter(
const grantedCount = COMPUTER_USE_PERMISSIONS.filter(
(permission) => stateById.get(permission.id) === 'granted'
).length
const allGranted = grantedCount === PERMISSIONS.length
const allGranted = grantedCount === COMPUTER_USE_PERMISSIONS.length
const checking = loading && states.length === 0
const setupUnavailable = helperUnavailableReason !== null
const resetAccessDisabled =
resetting || loading || states.length === 0 || pendingId !== null || setupUnavailable
const summaryTitle = checking
? 'Checking Computer Use access.'
: setupUnavailable
? 'Computer Use is unavailable.'
: allGranted
? 'Computer Use is ready.'
: 'Finish setup to use local apps.'
const summaryDescription = checking
? 'Orca is checking macOS privacy permissions for the Computer Use helper.'
: setupUnavailable
? `Computer Use permissions are unavailable because ${helperUnavailableReason}.`
: allGranted
? 'Agents can inspect and operate app windows when you ask.'
: `${PERMISSIONS.length - grantedCount} permission${
PERMISSIONS.length - grantedCount === 1 ? '' : 's'
} required before agents can operate app windows.`
const { title: summaryTitle, description: summaryDescription } = getComputerUseSummary({
checking,
setupUnavailable,
allGranted,
helperUnavailableReason,
requiredPermissionCount: COMPUTER_USE_PERMISSIONS.length - grantedCount
})
useEffect(() => {
mountedRef.current = true
@@ -323,7 +299,7 @@ export function ComputerUsePane(): React.JSX.Element {
<div className="space-y-2">
<div className="divide-y divide-border/60 rounded-lg border border-border/60">
{PERMISSIONS.map((permission) => {
{COMPUTER_USE_PERMISSIONS.map((permission) => {
const status = stateById.get(permission.id)
const pending = pendingId === permission.id
@@ -338,11 +314,11 @@ export function ComputerUsePane(): React.JSX.Element {
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-medium">{permission.label}</span>
<span
className={`rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider ${statusClass(
className={`rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider ${getComputerUsePermissionStatusClass(
status
)}`}
>
{statusLabel(status)}
{getComputerUsePermissionStatusLabel(status)}
</span>
</div>
<p className="text-xs text-muted-foreground">{permission.description}</p>
@@ -395,18 +371,23 @@ export function ComputerUsePane(): React.JSX.Element {
'auto.components.settings.ComputerUsePane.1735461723',
'Enables agents to inspect and operate local desktop apps.'
)}
command={COMPUTER_USE_SKILL_INSTALL_COMMAND}
command={skillInstallCommand}
terminalTitle="Computer Use setup"
terminalAriaLabel="Computer Use skill install terminal"
terminalWorktreeId="settings-computer-use-skill-terminal"
terminalWorktreeId={`settings-computer-use-skill-terminal-${skillRuntime.runtime}`}
terminalShellOverride={skillTerminalShellOverride}
installed={computerUseSkillDetected}
loading={computerUseSkillLoading}
error={computerUseSkillError}
icon={<MonitorCog className="size-5" />}
preInstallNotice={AGENT_SKILL_CLI_PREREQUISITE_NOTICE}
getPrerequisiteStatus={getSkillPrerequisiteStatus}
isPrerequisiteAvailable={isOrcaCliAvailableOnPath}
onBeforeOpenTerminal={async () => {
useAppStore.getState().recordFeatureInteraction('computer-use-setup')
await ensureOrcaCliAvailableForAgentSkillTerminal()
await (skillRuntime.runtime === 'wsl'
? ensureWslCliAvailableForAgentSkillTerminal()
: ensureOrcaCliAvailableForAgentSkillTerminal())
}}
onRecheck={refreshComputerUseSkill}
/>
@@ -1,9 +1,12 @@
import { useState } from 'react'
import { useMemo, useState } from 'react'
import { ArrowRightLeft, GitBranch, ListChecks, Workflow } from 'lucide-react'
import type { SkillDiscoveryTarget } from '../../../../shared/skills'
import type { GlobalSettings } from '../../../../shared/types'
import { ORCHESTRATION_SKILL_NAME } from '@/lib/agent-feature-install-commands'
import {
AGENT_SKILL_CLI_PREREQUISITE_NOTICE,
ensureOrcaCliAvailableForAgentSkillTerminal
ensureOrcaCliAvailableForAgentSkillTerminal,
isOrcaCliAvailableOnPath
} from '@/lib/agent-skill-cli-prerequisite'
import { ORCHESTRATION_SKILL_INSTALL_COMMAND } from '@/lib/orchestration-install-command'
import { getOrchestrationUsageExamples } from '@/lib/orchestration-usage-examples'
@@ -20,6 +23,15 @@ import { OrchestrationSkillAgentCoverage } from './OrchestrationSkillAgentCovera
import { OrchestrationExampleDialog } from './OrchestrationExamplesDialog'
import { OrchestrationSkillPromptDialog } from './OrchestrationSkillPromptDialog'
import { translate } from '@/i18n/i18n'
import {
buildSkillInstallCommandForRuntime,
ensureWslCliAvailableForAgentSkillTerminal,
getAgentSkillTerminalShellOverride,
getSelectedAgentRuntime,
getSkillDiscoveryTargetForRuntime,
type LocalAgentRuntime
} from './CliSkillRuntimeSetup'
import { getDesktopPlatformFromUserAgent } from './GeneralPane'
const EXAMPLE_ICONS = {
handoff: ArrowRightLeft,
@@ -29,11 +41,52 @@ const EXAMPLE_ICONS = {
'child-worktrees': Workflow
} as const
export function OrchestrationPane(): React.JSX.Element {
type OrchestrationPaneProps = {
currentPlatform?: string
settings?: GlobalSettings | null
wslSupportedPlatform?: boolean
wslAvailable?: boolean
wslCapabilitiesLoading?: boolean
}
function getOrchestrationSkillRuntime(props: OrchestrationPaneProps): LocalAgentRuntime {
if (!props.settings) {
return { runtime: 'host', label: 'This device' }
}
return getSelectedAgentRuntime(
props.settings,
props.wslSupportedPlatform ?? false,
props.wslAvailable ?? false,
props.wslCapabilitiesLoading ?? false
)
}
export function OrchestrationPane(props: OrchestrationPaneProps = {}): React.JSX.Element {
const searchQuery = useAppStore((s) => s.settingsSearchQuery)
const showOrchestration = matchesSettingsSearch(searchQuery, getOrchestrationPaneSearchEntries())
const [selectedExampleId, setSelectedExampleId] = useState<string | null>(null)
const [skillPromptOpen, setSkillPromptOpen] = useState(false)
const currentPlatform =
props.currentPlatform ??
(typeof navigator === 'undefined'
? 'other'
: getDesktopPlatformFromUserAgent(navigator.userAgent))
const skillRuntime = useMemo(() => getOrchestrationSkillRuntime(props), [props])
const skillDiscoveryTarget = useMemo<SkillDiscoveryTarget | undefined>(
() => getSkillDiscoveryTargetForRuntime(skillRuntime),
[skillRuntime]
)
const skillInstallCommand = buildSkillInstallCommandForRuntime(
ORCHESTRATION_SKILL_INSTALL_COMMAND,
skillRuntime
)
const skillTerminalShellOverride = props.settings
? getAgentSkillTerminalShellOverride(currentPlatform, props.settings, skillRuntime)
: undefined
const getSkillPrerequisiteStatus = () =>
skillRuntime.runtime === 'wsl'
? window.api.cli.getWslInstallStatus()
: window.api.cli.getInstallStatus()
const {
installed: orchestrationSkillDetected,
@@ -42,6 +95,7 @@ export function OrchestrationPane(): React.JSX.Element {
skills: discoveredSkills,
refresh: refreshOrchestrationSkill
} = useInstalledAgentSkill(ORCHESTRATION_SKILL_NAME, {
discoveryTarget: skillDiscoveryTarget,
sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS
})
@@ -71,18 +125,23 @@ export function OrchestrationPane(): React.JSX.Element {
'auto.components.settings.OrchestrationPane.9bedd2a6e5',
'Enables agents to hand off context and coordinate work through Orca.'
)}
command={ORCHESTRATION_SKILL_INSTALL_COMMAND}
command={skillInstallCommand}
terminalTitle="Orchestration setup"
terminalAriaLabel="Orchestration skill install terminal"
terminalWorktreeId="settings-orchestration-skill-terminal"
terminalWorktreeId={`settings-orchestration-skill-terminal-${skillRuntime.runtime}`}
terminalShellOverride={skillTerminalShellOverride}
installed={orchestrationSkillDetected}
loading={orchestrationSkillLoading}
error={orchestrationSkillError}
icon={<Workflow className="size-5" />}
preInstallNotice={AGENT_SKILL_CLI_PREREQUISITE_NOTICE}
getPrerequisiteStatus={getSkillPrerequisiteStatus}
isPrerequisiteAvailable={isOrcaCliAvailableOnPath}
onBeforeOpenTerminal={async () => {
useAppStore.getState().recordFeatureInteraction('agent-orchestration-setup')
await ensureOrcaCliAvailableForAgentSkillTerminal()
await (skillRuntime.runtime === 'wsl'
? ensureWslCliAvailableForAgentSkillTerminal()
: ensureOrcaCliAvailableForAgentSkillTerminal())
}}
actionHint={
<p className="text-[12px] leading-snug text-muted-foreground">
@@ -113,7 +172,7 @@ export function OrchestrationPane(): React.JSX.Element {
/>
<OrchestrationSkillPromptDialog
command={ORCHESTRATION_SKILL_INSTALL_COMMAND}
command={skillInstallCommand}
open={skillPromptOpen}
onOpenChange={setSkillPromptOpen}
/>
@@ -2,6 +2,7 @@
import { useCallback, useEffect, useMemo, useRef, useState, type MutableRefObject } from 'react'
import { toast } from 'sonner'
import type { GlobalSettings, OrcaHooks } from '../../../../shared/types'
import type { SkillDiscoveryTarget } from '../../../../shared/skills'
import type { SpeechModelState } from '../../../../shared/speech-types'
import type {
SourceControlAiSettings,
@@ -16,7 +17,7 @@ import { applyDocumentTheme } from '@/lib/document-theme'
import { useConfirmationDialog } from '@/components/confirmation-dialog'
import { SCROLLBACK_PRESETS_MB, getFallbackTerminalFonts } from './SettingsConstants'
import { DEFAULT_APP_FONT_FAMILY, getDefaultVoiceSettings } from '../../../../shared/constants'
import { GeneralPane } from './GeneralPane'
import { GeneralPane, getDesktopPlatformFromUserAgent } from './GeneralPane'
import { BrowserPane } from './BrowserPane'
import { AppearancePane } from './AppearancePane'
import { InputPane } from './InputPane'
@@ -85,6 +86,11 @@ import {
getRuntimeTargetIdentity
} from './settings-load-performance'
import { translate } from '@/i18n/i18n'
import {
getSelectedAgentRuntime,
getSkillDiscoveryTargetForRuntime,
type LocalAgentRuntime
} from './CliSkillRuntimeSetup'
const SETTINGS_NAV_GROUPS = [
{
@@ -161,6 +167,16 @@ function getSkillNavInstallStatus(skill: {
return skill.installed ? 'installed' : 'install'
}
function getSettingsAgentSkillRuntime(args: {
settings: GlobalSettings | null
isWindows: boolean
}): LocalAgentRuntime {
if (!args.settings) {
return { runtime: 'host', label: 'This device' }
}
return getSelectedAgentRuntime(args.settings, args.isWindows, args.isWindows, false)
}
function hasReadyVoiceModel(
settings: GlobalSettings,
modelStates: readonly SpeechModelState[]
@@ -256,10 +272,21 @@ function Settings(): React.JSX.Element {
const isMac = isMacUserAgent()
const isWebClient = isWebClientLocation()
const showDesktopOnlySettings = !isWebClient
const currentPlatform = getDesktopPlatformFromUserAgent(navigator.userAgent)
const agentSkillRuntime = useMemo(
() => getSettingsAgentSkillRuntime({ settings, isWindows }),
[settings, isWindows]
)
const agentSkillDiscoveryTarget = useMemo<SkillDiscoveryTarget | undefined>(
() => getSkillDiscoveryTargetForRuntime(agentSkillRuntime),
[agentSkillRuntime]
)
const orchestrationSkill = useInstalledAgentSkill(ORCHESTRATION_SKILL_NAME, {
discoveryTarget: agentSkillDiscoveryTarget,
sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS
})
const computerUseSkill = useInstalledAgentSkill(COMPUTER_USE_SKILL_NAME, {
discoveryTarget: agentSkillDiscoveryTarget,
enabled: showDesktopOnlySettings,
sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS
})
@@ -1041,7 +1068,15 @@ function Settings(): React.JSX.Element {
)}
searchEntries={getSectionSearchEntries('orchestration')}
>
{isSectionMounted('orchestration') ? <OrchestrationPane /> : null}
{isSectionMounted('orchestration') ? (
<OrchestrationPane
currentPlatform={currentPlatform}
settings={settings}
wslSupportedPlatform={wslSupportedPlatform}
wslAvailable={windowsTerminalCapabilities.wslAvailable}
wslCapabilitiesLoading={windowsTerminalCapabilities.isLoading}
/>
) : null}
</SettingsSection>
{showDesktopOnlySettings ? (
@@ -1058,7 +1093,15 @@ function Settings(): React.JSX.Element {
)}
searchEntries={getSectionSearchEntries('computer-use')}
>
{isSectionMounted('computer-use') ? <ComputerUsePane /> : null}
{isSectionMounted('computer-use') ? (
<ComputerUsePane
currentPlatform={currentPlatform}
settings={settings}
wslSupportedPlatform={wslSupportedPlatform}
wslAvailable={windowsTerminalCapabilities.wslAvailable}
wslCapabilitiesLoading={windowsTerminalCapabilities.isLoading}
/>
) : null}
</SettingsSection>
<SettingsSection
@@ -0,0 +1,66 @@
import type { ReactNode } from 'react'
import { Accessibility, Camera } from 'lucide-react'
import type {
ComputerUsePermissionId,
ComputerUsePermissionStatus
} from '../../../../shared/computer-use-permissions-types'
import { translate } from '@/i18n/i18n'
type PermissionDefinition = {
id: ComputerUsePermissionId
label: string
description: string
icon: ReactNode
}
export const COMPUTER_USE_PERMISSIONS: PermissionDefinition[] = [
{
id: 'accessibility',
get label() {
return translate('auto.components.settings.ComputerUsePane.6b5a2cd3a5', 'Accessibility')
},
get description() {
return translate(
'auto.components.settings.ComputerUsePane.4d03dec2d0',
'Read app interface trees and perform requested actions.'
)
},
icon: <Accessibility className="size-4" />
},
{
id: 'screenshots',
get label() {
return translate('auto.components.settings.ComputerUsePane.07bbe4c4cb', 'Screenshots')
},
get description() {
return translate(
'auto.components.settings.ComputerUsePane.0c9a33f468',
'Capture app windows so agents can inspect visual state.'
)
},
icon: <Camera className="size-4" />
}
]
export function getComputerUsePermissionStatusLabel(
status: ComputerUsePermissionStatus | undefined
): string {
switch (status) {
case 'granted':
return 'Granted'
case 'unsupported':
return 'macOS only'
case 'not-granted':
case undefined:
return 'Not enabled'
}
}
export function getComputerUsePermissionStatusClass(
status: ComputerUsePermissionStatus | undefined
): string {
if (status === 'granted') {
return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
}
return 'border-border bg-muted text-muted-foreground'
}
@@ -0,0 +1,21 @@
import type { GlobalSettings } from '../../../../shared/types'
import { getSelectedAgentRuntime, type LocalAgentRuntime } from './CliSkillRuntimeSetup'
export type ComputerUseSkillRuntimeInput = {
settings?: GlobalSettings | null
wslSupportedPlatform?: boolean
wslAvailable?: boolean
wslCapabilitiesLoading?: boolean
}
export function getComputerUseSkillRuntime(input: ComputerUseSkillRuntimeInput): LocalAgentRuntime {
if (!input.settings) {
return { runtime: 'host', label: 'This device' }
}
return getSelectedAgentRuntime(
input.settings,
input.wslSupportedPlatform ?? false,
input.wslAvailable ?? false,
input.wslCapabilitiesLoading ?? false
)
}
@@ -0,0 +1,45 @@
import { translate } from '@/i18n/i18n'
type ComputerUseSummaryInput = {
checking: boolean
setupUnavailable: boolean
allGranted: boolean
helperUnavailableReason: string | null
requiredPermissionCount: number
}
export function getComputerUseSummary({
checking,
setupUnavailable,
allGranted,
helperUnavailableReason,
requiredPermissionCount
}: ComputerUseSummaryInput): { title: string; description: string } {
if (checking) {
return {
title: 'Checking Computer Use access.',
description: 'Orca is checking macOS privacy permissions for the Computer Use helper.'
}
}
if (setupUnavailable) {
return {
title: 'Computer Use is unavailable.',
description: `Computer Use permissions are unavailable because ${helperUnavailableReason}.`
}
}
if (allGranted) {
return {
title: 'Computer Use is ready.',
description: 'Agents can inspect and operate app windows when you ask.'
}
}
return {
title: 'Finish setup to use local apps.',
description: translate(
'auto.components.settings.computerUseSummary.permissionsRequired',
`${requiredPermissionCount} permission${
requiredPermissionCount === 1 ? '' : 's'
} required before agents can operate app windows.`
)
}
}
@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest'
import {
buildWslInteractiveLoginShellCommand,
buildWslLoginShellCommand,
quotePosixShell
} from './wsl-login-shell-command'
describe('wsl login shell command helpers', () => {
it('quotes single quotes for POSIX shell arguments', () => {
expect(quotePosixShell("a'b")).toBe("'a'\\''b'")
})
it('runs commands through the distro user login shell', () => {
const command = buildWslLoginShellCommand("printf 'hello'")
expect(command).toContain('getent passwd')
expect(command).toContain('exec "$_orca_wsl_shell" -ilc')
expect(command).toContain("printf '\\''hello'\\''")
})
it('starts an interactive login shell without assuming bash', () => {
const command = buildWslInteractiveLoginShellCommand()
expect(command).toContain('getent passwd')
expect(command).toContain('if [ -z "$_orca_wsl_shell" ] || [ ! -x "$_orca_wsl_shell" ]; then')
expect(command).toContain('exec "$_orca_wsl_shell" -l')
})
})
+37
View File
@@ -0,0 +1,37 @@
export function quotePosixShell(value: string): string {
return `'${value.replace(/'/g, "'\\''")}'`
}
export function escapeWslShCommandForWindows(command: string): string {
return command.replace(/\$/g, '\\$')
}
export function buildWslLoginShellCommand(command: string): string {
const quotedCommand = quotePosixShell(command)
return [
'_orca_wsl_shell=$(getent passwd "$(id -un)" 2>/dev/null | cut -d: -f7)',
'if [ -z "$_orca_wsl_shell" ] || [ ! -x "$_orca_wsl_shell" ]; then',
' _orca_wsl_shell="${SHELL:-/bin/bash}"',
'fi',
'if [ -z "$_orca_wsl_shell" ] || [ ! -x "$_orca_wsl_shell" ]; then',
' _orca_wsl_shell=/bin/sh',
'fi',
'case "$(basename "$_orca_wsl_shell")" in',
` sh|dash) exec "$_orca_wsl_shell" -lc ${quotedCommand} ;;`,
` *) exec "$_orca_wsl_shell" -ilc ${quotedCommand} ;;`,
'esac'
].join('\n')
}
export function buildWslInteractiveLoginShellCommand(): string {
return [
'_orca_wsl_shell=$(getent passwd "$(id -un)" 2>/dev/null | cut -d: -f7)',
'if [ -z "$_orca_wsl_shell" ] || [ ! -x "$_orca_wsl_shell" ]; then',
' _orca_wsl_shell="${SHELL:-/bin/bash}"',
'fi',
'if [ -z "$_orca_wsl_shell" ] || [ ! -x "$_orca_wsl_shell" ]; then',
' _orca_wsl_shell=/bin/sh',
'fi',
'exec "$_orca_wsl_shell" -l'
].join('\n')
}