From 711838207e27689bf17a182a82b8ffc41329fc13 Mon Sep 17 00:00:00 2001 From: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:50:39 -0700 Subject: [PATCH] fix(runtime): harden orphan recovery validation --- config/reliability-gates.jsonc | 1 + src/main/ipc/runtime-environments.ts | 10 +--- src/main/ipc/settings.test.ts | 17 ++++++ src/main/ipc/settings.ts | 6 +- src/main/runtime/orca-runtime.test.ts | 45 +++++++++----- src/main/runtime/orca-runtime.ts | 2 +- .../runtime/rpc/methods/terminal-orphan.ts | 8 ++- src/main/runtime/rpc/schemas.test.ts | 5 ++ .../settings/RuntimeEnvironmentsPane.tsx | 11 +++- src/renderer/src/i18n/locales/en.json | 31 ++++++++-- src/renderer/src/i18n/locales/es.json | 49 +++++++++++++-- src/renderer/src/i18n/locales/ja.json | 49 +++++++++++++-- src/renderer/src/i18n/locales/ko.json | 49 +++++++++++++-- src/renderer/src/i18n/locales/zh.json | 49 +++++++++++++-- .../src/runtime/web-session-tabs-sync.ts | 60 ++++++++++++------- src/renderer/src/store/slices/browser.ts | 5 +- src/shared/runtime-types.ts | 3 +- 17 files changed, 318 insertions(+), 82 deletions(-) diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 1505f495fad..d0c5ee6782f 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -1442,6 +1442,7 @@ "src/renderer/src/runtime/web-session-tabs-sync.test.ts", "tests/e2e/remote-terminal-tab-retirement.unit.test.ts", "src/main/runtime/orca-runtime.test.ts", + "src/main/runtime/terminal-orphan-owner.test.ts", "src/main/runtime/terminal-orphan-topology.test.ts", "src/renderer/src/runtime/web-session-terminal-orphan-recovery.test.ts", "src/renderer/src/runtime/web-session-terminal-orphan-mixed-version.test.ts" diff --git a/src/main/ipc/runtime-environments.ts b/src/main/ipc/runtime-environments.ts index 5eadd75fe0d..9662b8ab598 100644 --- a/src/main/ipc/runtime-environments.ts +++ b/src/main/ipc/runtime-environments.ts @@ -79,9 +79,7 @@ export function registerRuntimeEnvironmentHandlers(store: Store): void { } ipcMain.removeAllListeners('runtimeEnvironments:subscriptionBinary') - ipcMain.handle('runtimeEnvironments:list', (): PublicKnownRuntimeEnvironment[] => - listPublicRuntimeEnvironments() - ) + ipcMain.handle('runtimeEnvironments:list', listPublicRuntimeEnvironments) ipcMain.handle( 'runtimeEnvironments:addFromPairingCode', ( @@ -91,10 +89,8 @@ export function registerRuntimeEnvironmentHandlers(store: Store): void { environment: redactRuntimeEnvironment(addEnvironmentFromPairingCode(getUserDataPath(), args)) }) ) - ipcMain.handle( - 'runtimeEnvironments:resolve', - (_event, args: { selector: string }): PublicKnownRuntimeEnvironment => - redactRuntimeEnvironment(resolveEnvironment(getUserDataPath(), args.selector)) + ipcMain.handle('runtimeEnvironments:resolve', (_event, args: { selector: string }) => + redactRuntimeEnvironment(resolveEnvironment(getUserDataPath(), args.selector)) ) ipcMain.handle( 'runtimeEnvironments:remove', diff --git a/src/main/ipc/settings.test.ts b/src/main/ipc/settings.test.ts index ef293d20626..c6d6a940b23 100644 --- a/src/main/ipc/settings.test.ts +++ b/src/main/ipc/settings.test.ts @@ -9,6 +9,7 @@ const { previewGhosttyImportMock, previewWarpThemeImportMock, prepareLocalWorktreeRootsForReposMock, + resolveEnvironmentMock, rebuildAppMenuMock } = vi.hoisted(() => ({ applyAppIconMock: vi.fn(), @@ -19,10 +20,12 @@ const { previewGhosttyImportMock: vi.fn(), previewWarpThemeImportMock: vi.fn(), prepareLocalWorktreeRootsForReposMock: vi.fn(), + resolveEnvironmentMock: vi.fn(), rebuildAppMenuMock: vi.fn() })) vi.mock('electron', () => ({ + app: { getPath: vi.fn(() => '/test/user-data') }, BrowserWindow: { getAllWindows: browserWindowGetAllWindowsMock }, ipcMain: { handle: handleMock, on: onMock }, nativeTheme: { themeSource: 'system' } @@ -52,6 +55,10 @@ vi.mock('../menu/register-app-menu', () => ({ rebuildAppMenu: rebuildAppMenuMock })) +vi.mock('../../shared/runtime-environment-store', () => ({ + resolveEnvironment: resolveEnvironmentMock +})) + import { registerSettingsHandlers } from './settings' const settingsInvokeEvent = { sender: { id: 1 } } @@ -79,6 +86,12 @@ describe('registerSettingsHandlers', () => { previewGhosttyImportMock.mockClear() previewWarpThemeImportMock.mockClear() prepareLocalWorktreeRootsForReposMock.mockReset().mockResolvedValue(undefined) + resolveEnvironmentMock.mockReset().mockImplementation((_userDataPath, selector) => { + if (selector !== 'windows-2') { + throw new Error('Runtime environment not found') + } + return { id: selector } + }) rebuildAppMenuMock.mockClear() browserWindowGetAllWindowsMock.mockReset() store.getSettings.mockReset() @@ -143,6 +156,10 @@ describe('registerSettingsHandlers', () => { expect(() => handler(settingsInvokeEvent, { environmentId: 42 as never })).toThrow( 'Invalid Active Server preference' ) + expect(() => handler(settingsInvokeEvent, { environmentId: 'does-not-exist' })).toThrow( + 'Runtime environment not found' + ) + expect(store.updateSettings).toHaveBeenCalledTimes(1) }) it('applies bot-author deltas against the authoritative settings snapshot', () => { diff --git a/src/main/ipc/settings.ts b/src/main/ipc/settings.ts index e5c1081e1c4..8ee9ac067e7 100644 --- a/src/main/ipc/settings.ts +++ b/src/main/ipc/settings.ts @@ -1,4 +1,4 @@ -import { BrowserWindow, ipcMain, nativeTheme } from 'electron' +import { app, BrowserWindow, ipcMain, nativeTheme } from 'electron' import type { Store } from '../persistence' import type { GlobalSettings, PersistedState } from '../../shared/types' import { listSystemFontFamilies } from '../system-fonts' @@ -22,6 +22,7 @@ import { normalizeTerminalLineHeight } from '../../shared/terminal-line-height-s import { prepareLocalWorktreeRootsForRepos } from '../worktree-root-preparation' import { scheduleCurrentWorktreeBaseDirectoryWatcherSync } from './worktree-base-directory-watcher' import { applyPRBotAuthorOverride } from '../../shared/pr-bot-author-overrides' +import { resolveEnvironment } from '../../shared/runtime-environment-store' // Why: the whitelist is the source-of-truth for which keys we emit on. Casting // to a Set once at module load lets the IPC handler's per-key membership @@ -216,6 +217,9 @@ export function registerSettingsHandlers( throw new Error('Invalid Active Server preference') } const environmentId = requestedEnvironmentId?.trim() || null + if (environmentId !== null) { + resolveEnvironment(app.getPath('userData'), environmentId) + } return store.updateSettings( { activeRuntimeEnvironmentId: environmentId }, { notifyListeners: true, originWebContentsId: event.sender.id } diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index c58b05f29bc..5881f944a5c 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -15942,7 +15942,8 @@ describe('OrcaRuntimeService', () => { terminalHandle, title, cwd: TEST_WORKTREE_PATH, - worktreeId: TEST_WORKTREE_ID + worktreeId: TEST_WORKTREE_ID, + wslDistro: null })) }) const before = await runtime.listTerminals(`id:${TEST_WORKTREE_ID}`) @@ -16113,7 +16114,8 @@ describe('OrcaRuntimeService', () => { terminalHandle: 'term_new', title: 'Terminal 3', cwd: TEST_WORKTREE_PATH, - worktreeId: TEST_WORKTREE_ID + worktreeId: TEST_WORKTREE_ID, + wslDistro: null }, { id: 'pty-agent', @@ -16121,7 +16123,8 @@ describe('OrcaRuntimeService', () => { terminalHandle: 'term_agent', title: 'Claude', cwd: TEST_WORKTREE_PATH, - worktreeId: TEST_WORKTREE_ID + worktreeId: TEST_WORKTREE_ID, + wslDistro: null }, { id: 'pty-setup', @@ -16129,7 +16132,8 @@ describe('OrcaRuntimeService', () => { terminalHandle: 'term_setup', title: 'Setup', cwd: TEST_WORKTREE_PATH, - worktreeId: TEST_WORKTREE_ID + worktreeId: TEST_WORKTREE_ID, + wslDistro: null }, { id: 'pty-shell', @@ -16137,7 +16141,8 @@ describe('OrcaRuntimeService', () => { terminalHandle: 'term_shell', title: 'Shell', cwd: TEST_WORKTREE_PATH, - worktreeId: TEST_WORKTREE_ID + worktreeId: TEST_WORKTREE_ID, + wslDistro: null } ] }) @@ -16280,7 +16285,8 @@ describe('OrcaRuntimeService', () => { terminalHandle: 'term_alias', title: 'Alias shell', cwd: TEST_WORKTREE_PATH, - worktreeId: TEST_WORKTREE_ID + worktreeId: TEST_WORKTREE_ID, + wslDistro: null } ] }) @@ -16337,7 +16343,8 @@ describe('OrcaRuntimeService', () => { terminalHandle: 'term_current', title: 'Current shell', cwd: TEST_WORKTREE_PATH, - worktreeId: TEST_WORKTREE_ID + worktreeId: TEST_WORKTREE_ID, + wslDistro: null } ] : [] @@ -16412,7 +16419,8 @@ describe('OrcaRuntimeService', () => { terminalHandle: 'term_cas', title: 'shell', cwd: TEST_WORKTREE_PATH, - worktreeId: TEST_WORKTREE_ID + worktreeId: TEST_WORKTREE_ID, + wslDistro: null } ] }) @@ -16456,7 +16464,8 @@ describe('OrcaRuntimeService', () => { terminalHandle: 'term_wrong_owner', title: 'shell', cwd: TEST_WORKTREE_PATH, - worktreeId: TEST_WORKTREE_ID + worktreeId: TEST_WORKTREE_ID, + wslDistro: null } ] }) @@ -16495,7 +16504,8 @@ describe('OrcaRuntimeService', () => { terminalHandle: 'term_reused', title: 'shell', cwd: TEST_WORKTREE_PATH, - worktreeId: TEST_WORKTREE_ID + worktreeId: TEST_WORKTREE_ID, + wslDistro: null }, { id: 'pty-second', @@ -16503,7 +16513,8 @@ describe('OrcaRuntimeService', () => { terminalHandle: 'term_reused', title: 'shell', cwd: TEST_WORKTREE_PATH, - worktreeId: TEST_WORKTREE_ID + worktreeId: TEST_WORKTREE_ID, + wslDistro: null } ] }) @@ -16620,7 +16631,8 @@ describe('OrcaRuntimeService', () => { terminalHandle, title: id, cwd: TEST_WORKTREE_PATH, - worktreeId: TEST_WORKTREE_ID + worktreeId: TEST_WORKTREE_ID, + wslDistro: null })) }) @@ -16720,7 +16732,8 @@ describe('OrcaRuntimeService', () => { terminalHandle: 'term_old', title: 'old', cwd: TEST_WORKTREE_PATH, - worktreeId: TEST_WORKTREE_ID + worktreeId: TEST_WORKTREE_ID, + wslDistro: null } runtime.setPtyController({ write: () => true, @@ -16790,7 +16803,8 @@ describe('OrcaRuntimeService', () => { terminalHandle: 'term_orphan', title: 'orphan', cwd: TEST_WORKTREE_PATH, - worktreeId: TEST_WORKTREE_ID + worktreeId: TEST_WORKTREE_ID, + wslDistro: null } ] }) @@ -16859,7 +16873,8 @@ describe('OrcaRuntimeService', () => { terminalHandle: 'term_duplicate', title: 'duplicate', cwd: TEST_WORKTREE_PATH, - worktreeId: TEST_WORKTREE_ID + worktreeId: TEST_WORKTREE_ID, + wslDistro: null } ] }) diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 200522ba500..1a214ef445c 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -13057,7 +13057,7 @@ export class OrcaRuntimeService { this.hydrateHeadlessMobileSessionTabsFromWorkspaceSession(worktree.id, { force: true, allowAttachedWindow: true, - onlyServeOwnedTerminals: true + onlyRuntimeOwnedTerminals: true }) this.notifyMobileSessionTabsChanged(worktree.id) return { diff --git a/src/main/runtime/rpc/methods/terminal-orphan.ts b/src/main/runtime/rpc/methods/terminal-orphan.ts index 5ca6fe21705..979c6aa747c 100644 --- a/src/main/runtime/rpc/methods/terminal-orphan.ts +++ b/src/main/runtime/rpc/methods/terminal-orphan.ts @@ -1,5 +1,6 @@ import { z } from 'zod' import type { TabGroupLayoutNode } from '../../../../shared/types' +import { isPtyIncarnationId, type PtyIncarnationId } from '../../../../shared/pty-incarnation' import { defineMethod, type RpcAnyMethod } from '../core' import { OptionalString, requiredString } from '../schemas' import { TerminalPaneLayoutNodeSchema } from './session-tabs-schemas' @@ -78,6 +79,11 @@ const TerminalOrphanTopology = z.object({ groupLayout: TerminalOrphanGroupLayout.optional() }) +const TerminalOrphanIncarnationId = z.custom( + isPtyIncarnationId, + 'Invalid PTY incarnation' +) + const TerminalAdoptOrphans = z.object({ worktree: requiredString('Missing worktree selector').pipe(z.string().max(32_768)), expectedTopologyRevision: z.number().int().nonnegative(), @@ -86,7 +92,7 @@ const TerminalAdoptOrphans = z.object({ z.object({ terminal: requiredString('Missing terminal handle').pipe(z.string().max(256)), ptyId: requiredString('Missing PTY id').pipe(z.string().max(8_192)), - incarnationId: requiredString('Missing PTY incarnation').pipe(z.string().max(256)), + incarnationId: TerminalOrphanIncarnationId, tabId: requiredString('Missing tab id').pipe(z.string().max(256)), leafId: requiredString('Missing leaf id').pipe(z.string().max(128)) }) diff --git a/src/main/runtime/rpc/schemas.test.ts b/src/main/runtime/rpc/schemas.test.ts index bc7dc5ae690..8ba47496545 100644 --- a/src/main/runtime/rpc/schemas.test.ts +++ b/src/main/runtime/rpc/schemas.test.ts @@ -123,6 +123,11 @@ describe('RPC optional pipe schemas', () => { expectedTopologyRevision: 4, claims: [{ ...claim, incarnationId: '' }] }) + expectRejects(adopt, { + worktree: 'id:repo::/worktree', + expectedTopologyRevision: 4, + claims: [{ ...claim, incarnationId: 'i'.repeat(129) }] + }) expectRejects(adopt, { worktree: 'id:repo::/worktree', expectedTopologyRevision: 4, diff --git a/src/renderer/src/components/settings/RuntimeEnvironmentsPane.tsx b/src/renderer/src/components/settings/RuntimeEnvironmentsPane.tsx index 6af2bc2f3f7..488704193d2 100644 --- a/src/renderer/src/components/settings/RuntimeEnvironmentsPane.tsx +++ b/src/renderer/src/components/settings/RuntimeEnvironmentsPane.tsx @@ -512,7 +512,12 @@ export function RuntimeEnvironmentsPane({ try { if (allowLocalRuntime && settings.activeRuntimeEnvironmentId === environment.id) { if (mountedRef.current) { - setRemoveError('Choose another Active Server in Advanced before removing this server.') + setRemoveError( + translate( + 'auto.components.settings.RuntimeEnvironmentsPane.removeActiveServerBlocked', + 'Choose another Active Server in Advanced before removing this server.' + ) + ) } return false } @@ -1382,8 +1387,8 @@ export function RuntimeEnvironmentsPane({ {removingActiveServer ? allowLocalRuntime ? translate( - 'auto.components.settings.RuntimeEnvironmentsPane.9f7665a01b', - 'Removing the active server first switches Orca back to Local desktop. Existing host sessions are left alone.' + 'auto.components.settings.RuntimeEnvironmentsPane.removeActiveServerDescription', + 'Choose another Active Server in Advanced before removing this server. Existing host sessions are left alone.' ) : translate( 'auto.components.settings.RuntimeEnvironmentsPane.b2fda48c39', diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 6c586a405f0..d71288e7a8c 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -204,7 +204,8 @@ "slices": { "browser": { "d175274b6d": "New Browser Tab", - "08fc23631d": "Browser" + "08fc23631d": "Browser", + "remoteCookieImportUnavailable": "Manual cookie file import is unavailable while a remote runtime is active." }, "editor": { "dcb521ed29": "This file is in a conflict state, but no working-tree file is available to edit.", @@ -1273,7 +1274,17 @@ "noRunTargets": "No run targets are ready for this project.", "perWorkspaceEnvHint": "Provision an on-demand environment from a recipe", "branchName": "Branch name", - "branchNamePlaceholder": "feature/my-branch" + "branchNamePlaceholder": "feature/my-branch", + "connectTimedOut": "Connection timed out. It may still be connecting in the background.", + "connectingHost": "Connecting…", + "connectHost": "Connect", + "addHost": "Add host", + "addHostHint": "Register another machine or Orca server", + "addSshHost": "Add SSH host", + "addSshHostHint": "Use an existing machine over SSH", + "addRemoteOrcaServer": "Add Remote Orca Server", + "addRemoteOrcaServerHint": "Pair another Orca runtime", + "hostConnectionFailed": "Connection failed" }, "NewWorkspaceComposerModal": { "createWorktree": "Create worktree", @@ -4581,7 +4592,14 @@ "worktree": { "flow": { "c460fecc4a": "Failed to sleep some workspaces", - "8bc3fc0671": "Failed to sleep workspace" + "8bc3fc0671": "Failed to sleep workspace", + "legacy": { + "unverified": "The older host runtime could not confirm terminal shutdown. The workspace was kept open; update the host and try again." + }, + "host": { + "unverified": "The host could not confirm terminal shutdown. The workspace was kept open; check the connection and try again." + }, + "retry": "The workspace was kept open. Try again; if the problem continues, check the host connection." } } }, @@ -4848,7 +4866,8 @@ "sshImportSynced": "Synced {{value0}} host{{value1}}.", "sshImportFailed": "Failed to import SSH config.", "importing": "Importing...", - "importSshConfig": "Import ~/.ssh/config" + "importSshConfig": "Import ~/.ssh/config", + "advanced": "Advanced" }, "ForgetSshWorkspaceDialog": { "reconnectFailed": "Reconnection failed", @@ -6514,7 +6533,9 @@ "updateServer": "Update", "reviewServerUpdates": "Check for Server Updates", "updatingServers": "Updating servers…", - "orcaVersion": "Orca v{{value0}}" + "orcaVersion": "Orca v{{value0}}", + "removeActiveServerBlocked": "Choose another Active Server in Advanced before removing this server.", + "removeActiveServerDescription": "Choose another Active Server in Advanced before removing this server. Existing host sessions are left alone." }, "RuntimePairingGeneratedUrlRows": { "0495f68959": "Copy {{value0}}" diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 3d05eff9fa1..f759aef8922 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -181,7 +181,8 @@ "slices": { "browser": { "d175274b6d": "Nueva pestaña del navegador", - "08fc23631d": "Navegador" + "08fc23631d": "Navegador", + "remoteCookieImportUnavailable": "Manual cookie file import is unavailable while a remote runtime is active." }, "editor": { "dcb521ed29": "Este archivo está en conflicto, pero no hay una copia editable en el worktree.", @@ -1250,7 +1251,17 @@ "noRunTargets": "No hay destinos de ejecución listos para este proyecto.", "perWorkspaceEnvHint": "Provisiona un entorno bajo demanda a partir de una receta", "branchName": "Nombre de rama", - "branchNamePlaceholder": "feature/my-branch" + "branchNamePlaceholder": "feature/my-branch", + "connectTimedOut": "Connection timed out. It may still be connecting in the background.", + "connectingHost": "Connecting…", + "connectHost": "Connect", + "addHost": "Add host", + "addHostHint": "Register another machine or Orca server", + "addSshHost": "Add SSH host", + "addSshHostHint": "Use an existing machine over SSH", + "addRemoteOrcaServer": "Add Remote Orca Server", + "addRemoteOrcaServerHint": "Pair another Orca runtime", + "hostConnectionFailed": "Connection failed" }, "NewWorkspaceComposerModal": { "createWorktree": "Crear worktree", @@ -4423,7 +4434,23 @@ "0bed8727db": "Puede que se haya movido o eliminado. Actualiza los espacios de trabajo o quítalo de Orca.", "3921d3d9a5": "No se encontró la carpeta del espacio de trabajo.", "f387af445b": "La ruta del espacio de trabajo no es una ruta local válida.", - "3ec372b664": "gestor de archivos" + "3ec372b664": "gestor de archivos", + "localOnly": "Local only", + "remoteSsh": "Remote SSH", + "remoteRuntimeUnsupported": "Opening this path in a local app is not available.", + "remoteRuntimeUnsupportedDetail": "Switch to a local or SSH workspace, then try again.", + "sshTargetNotFound": "SSH host is no longer available.", + "sshTargetNotFoundDetail": "Refresh workspaces or reconnect the host, then try again.", + "sshTargetInvalid": "SSH host configuration is incomplete.", + "sshTargetInvalidDetail": "Edit or reconnect the SSH host, then try again.", + "sshAliasRequired": "VS Code needs an SSH config alias for this host.", + "sshAliasRequiredDetail": "Add a Host alias for {{host}}:{{port}} to your local SSH config, reconnect the workspace, then try again.", + "remoteEditorUnsupported": "This app cannot open SSH workspaces.", + "remoteEditorUnsupportedDetail": "Choose VS Code or use the app locally.", + "remotePathInvalid": "Path is not valid for the SSH host.", + "remotePathInvalidDetail": "Refresh the workspace before trying again.", + "remoteLaunchFailed": "Could not open the path in VS Code.", + "remoteLaunchFailedDetail": "Check the VS Code command configured on this machine." }, "WorktreeTitleInlineRename": { "2f42ae024f": "No leído:", @@ -4519,7 +4546,14 @@ "worktree": { "flow": { "c460fecc4a": "No se pudieron poner en reposo algunos espacios de trabajo", - "8bc3fc0671": "No se pudo poner en reposo el espacio de trabajo" + "8bc3fc0671": "No se pudo poner en reposo el espacio de trabajo", + "legacy": { + "unverified": "The older host runtime could not confirm terminal shutdown. The workspace was kept open; update the host and try again." + }, + "host": { + "unverified": "The host could not confirm terminal shutdown. The workspace was kept open; check the connection and try again." + }, + "retry": "The workspace was kept open. Try again; if the problem continues, check the host connection." } } }, @@ -4809,7 +4843,8 @@ "sshImportFailed": "No se pudo importar la configuración SSH.", "importing": "Importando...", "importSshConfig": "Importar ~/.ssh/config", - "sshPersistenceDefault": "Los terminales remotos en este host seguirán activos hasta que los cierres o restablezcas el relay." + "sshPersistenceDefault": "Los terminales remotos en este host seguirán activos hasta que los cierres o restablezcas el relay.", + "advanced": "Advanced" }, "ForgetSshWorkspaceDialog": { "reconnectFailed": "Error de reconexión", @@ -6437,7 +6472,9 @@ "versionUnavailable": "Orca version unavailable", "updateServer": "Update", "reviewServerUpdates": "Server updates", - "orcaVersion": "Orca v{{value0}}" + "orcaVersion": "Orca v{{value0}}", + "removeActiveServerBlocked": "Choose another Active Server in Advanced before removing this server.", + "removeActiveServerDescription": "Choose another Active Server in Advanced before removing this server. Existing host sessions are left alone." }, "RuntimePairingGeneratedUrlRows": { "0495f68959": "Copiar {{value0}}" diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 2ad43b89f8d..4cf3afc3085 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -181,7 +181,8 @@ "slices": { "browser": { "d175274b6d": "新規ブラウザタブ", - "08fc23631d": "ブラウザ" + "08fc23631d": "ブラウザ", + "remoteCookieImportUnavailable": "Manual cookie file import is unavailable while a remote runtime is active." }, "editor": { "dcb521ed29": "このファイルは競合状態にありますが、編集できる作業ツリー ファイルがありません。", @@ -1250,7 +1251,17 @@ "noRunTargets": "このプロジェクトには実行ターゲットが準備されていません。", "perWorkspaceEnvHint": "レシピからオンデマンド環境をプロビジョニングする", "branchName": "ブランチ名", - "branchNamePlaceholder": "feature/my-branch" + "branchNamePlaceholder": "feature/my-branch", + "connectTimedOut": "Connection timed out. It may still be connecting in the background.", + "connectingHost": "Connecting…", + "connectHost": "Connect", + "addHost": "Add host", + "addHostHint": "Register another machine or Orca server", + "addSshHost": "Add SSH host", + "addSshHostHint": "Use an existing machine over SSH", + "addRemoteOrcaServer": "Add Remote Orca Server", + "addRemoteOrcaServerHint": "Pair another Orca runtime", + "hostConnectionFailed": "Connection failed" }, "NewWorkspaceComposerModal": { "createWorktree": "ワークツリーを作成する", @@ -4404,7 +4415,23 @@ "0bed8727db": "移動または削除された可能性があります。ワークスペースを更新するか、Orca からワークスペースを削除します。", "3921d3d9a5": "ワークスペースフォルダーが見つかりませんでした。", "f387af445b": "ワークスペース パスは有効なローカル パスではありません。", - "3ec372b664": "ファイルマネージャー" + "3ec372b664": "ファイルマネージャー", + "localOnly": "Local only", + "remoteSsh": "Remote SSH", + "remoteRuntimeUnsupported": "Opening this path in a local app is not available.", + "remoteRuntimeUnsupportedDetail": "Switch to a local or SSH workspace, then try again.", + "sshTargetNotFound": "SSH host is no longer available.", + "sshTargetNotFoundDetail": "Refresh workspaces or reconnect the host, then try again.", + "sshTargetInvalid": "SSH host configuration is incomplete.", + "sshTargetInvalidDetail": "Edit or reconnect the SSH host, then try again.", + "sshAliasRequired": "VS Code needs an SSH config alias for this host.", + "sshAliasRequiredDetail": "Add a Host alias for {{host}}:{{port}} to your local SSH config, reconnect the workspace, then try again.", + "remoteEditorUnsupported": "This app cannot open SSH workspaces.", + "remoteEditorUnsupportedDetail": "Choose VS Code or use the app locally.", + "remotePathInvalid": "Path is not valid for the SSH host.", + "remotePathInvalidDetail": "Refresh the workspace before trying again.", + "remoteLaunchFailed": "Could not open the path in VS Code.", + "remoteLaunchFailedDetail": "Check the VS Code command configured on this machine." }, "WorktreeTitleInlineRename": { "2f42ae024f": "未読:", @@ -4500,7 +4527,14 @@ "worktree": { "flow": { "c460fecc4a": "一部のワークスペースのスリープに失敗しました", - "8bc3fc0671": "ワークスペースのスリープに失敗しました" + "8bc3fc0671": "ワークスペースのスリープに失敗しました", + "legacy": { + "unverified": "The older host runtime could not confirm terminal shutdown. The workspace was kept open; update the host and try again." + }, + "host": { + "unverified": "The host could not confirm terminal shutdown. The workspace was kept open; check the connection and try again." + }, + "retry": "The workspace was kept open. Try again; if the problem continues, check the host connection." } } }, @@ -4809,7 +4843,8 @@ "sshImportFailed": "SSH 構成のインポートに失敗しました。", "importing": "インポート中...", "importSshConfig": "~/.ssh/config をインポートします", - "sshPersistenceDefault": "このホスト上のリモートターミナルは、終了するかリレーをリセットするまで動作し続けます。" + "sshPersistenceDefault": "このホスト上のリモートターミナルは、終了するかリレーをリセットするまで動作し続けます。", + "advanced": "Advanced" }, "ForgetSshWorkspaceDialog": { "reconnectFailed": "再接続に失敗しました", @@ -6459,7 +6494,9 @@ "versionUnavailable": "Orca version unavailable", "updateServer": "Update", "reviewServerUpdates": "Server updates", - "orcaVersion": "Orca v{{value0}}" + "orcaVersion": "Orca v{{value0}}", + "removeActiveServerBlocked": "Choose another Active Server in Advanced before removing this server.", + "removeActiveServerDescription": "Choose another Active Server in Advanced before removing this server. Existing host sessions are left alone." }, "RuntimePairingGeneratedUrlRows": { "0495f68959": "{{value0}}をコピー" diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 7c53f7439f1..99ca1451dae 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -181,7 +181,8 @@ "slices": { "browser": { "d175274b6d": "새 브라우저 탭", - "08fc23631d": "브라우저" + "08fc23631d": "브라우저", + "remoteCookieImportUnavailable": "Manual cookie file import is unavailable while a remote runtime is active." }, "editor": { "dcb521ed29": "이 파일은 충돌 상태에 있지만 편집할 수 있는 작업 트리 파일이 없습니다.", @@ -1250,7 +1251,17 @@ "noRunTargets": "이 프로젝트에는 실행 대상이 준비되어 있지 않습니다.", "perWorkspaceEnvHint": "레시피에서 온디맨드 환경 프로비저닝", "branchName": "브랜치 이름", - "branchNamePlaceholder": "feature/my-branch" + "branchNamePlaceholder": "feature/my-branch", + "connectTimedOut": "Connection timed out. It may still be connecting in the background.", + "connectingHost": "Connecting…", + "connectHost": "Connect", + "addHost": "Add host", + "addHostHint": "Register another machine or Orca server", + "addSshHost": "Add SSH host", + "addSshHostHint": "Use an existing machine over SSH", + "addRemoteOrcaServer": "Add Remote Orca Server", + "addRemoteOrcaServerHint": "Pair another Orca runtime", + "hostConnectionFailed": "Connection failed" }, "NewWorkspaceComposerModal": { "createWorktree": "작업 트리 만들기", @@ -4404,7 +4415,23 @@ "0bed8727db": "이동되었거나 삭제되었을 수 있습니다. 워크스페이스를 새로고침하거나 Orca에서 제거하세요.", "3921d3d9a5": "워크스페이스 폴더를 찾을 수 없습니다.", "f387af445b": "워크스페이스 경로가 유효한 로컬 경로가 아닙니다.", - "3ec372b664": "파일 관리자" + "3ec372b664": "파일 관리자", + "localOnly": "Local only", + "remoteSsh": "Remote SSH", + "remoteRuntimeUnsupported": "Opening this path in a local app is not available.", + "remoteRuntimeUnsupportedDetail": "Switch to a local or SSH workspace, then try again.", + "sshTargetNotFound": "SSH host is no longer available.", + "sshTargetNotFoundDetail": "Refresh workspaces or reconnect the host, then try again.", + "sshTargetInvalid": "SSH host configuration is incomplete.", + "sshTargetInvalidDetail": "Edit or reconnect the SSH host, then try again.", + "sshAliasRequired": "VS Code needs an SSH config alias for this host.", + "sshAliasRequiredDetail": "Add a Host alias for {{host}}:{{port}} to your local SSH config, reconnect the workspace, then try again.", + "remoteEditorUnsupported": "This app cannot open SSH workspaces.", + "remoteEditorUnsupportedDetail": "Choose VS Code or use the app locally.", + "remotePathInvalid": "Path is not valid for the SSH host.", + "remotePathInvalidDetail": "Refresh the workspace before trying again.", + "remoteLaunchFailed": "Could not open the path in VS Code.", + "remoteLaunchFailedDetail": "Check the VS Code command configured on this machine." }, "WorktreeTitleInlineRename": { "2f42ae024f": "읽지 않음:", @@ -4500,7 +4527,14 @@ "worktree": { "flow": { "c460fecc4a": "일부 워크스페이스를 절전 모드로 전환하지 못했습니다.", - "8bc3fc0671": "워크스페이스를 절전 모드로 전환하지 못했습니다." + "8bc3fc0671": "워크스페이스를 절전 모드로 전환하지 못했습니다.", + "legacy": { + "unverified": "The older host runtime could not confirm terminal shutdown. The workspace was kept open; update the host and try again." + }, + "host": { + "unverified": "The host could not confirm terminal shutdown. The workspace was kept open; check the connection and try again." + }, + "retry": "The workspace was kept open. Try again; if the problem continues, check the host connection." } } }, @@ -4809,7 +4843,8 @@ "sshImportFailed": "SSH 구성을 가져오지 못했습니다.", "importing": "가져오는 중...", "importSshConfig": "~/.ssh/config 가져오기", - "sshPersistenceDefault": "이 호스트의 원격 터미널은 종료하거나 릴레이를 재설정할 때까지 계속 실행됩니다." + "sshPersistenceDefault": "이 호스트의 원격 터미널은 종료하거나 릴레이를 재설정할 때까지 계속 실행됩니다.", + "advanced": "Advanced" }, "ForgetSshWorkspaceDialog": { "reconnectFailed": "재연결 실패", @@ -6422,7 +6457,9 @@ "versionUnavailable": "Orca version unavailable", "updateServer": "Update", "reviewServerUpdates": "Server updates", - "orcaVersion": "Orca v{{value0}}" + "orcaVersion": "Orca v{{value0}}", + "removeActiveServerBlocked": "Choose another Active Server in Advanced before removing this server.", + "removeActiveServerDescription": "Choose another Active Server in Advanced before removing this server. Existing host sessions are left alone." }, "RuntimePairingGeneratedUrlRows": { "0495f68959": "{{value0}} 복사" diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 5c8e5787248..07aa9ee81e7 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -181,7 +181,8 @@ "slices": { "browser": { "d175274b6d": "新浏览器选项卡", - "08fc23631d": "浏览器" + "08fc23631d": "浏览器", + "remoteCookieImportUnavailable": "Manual cookie file import is unavailable while a remote runtime is active." }, "editor": { "dcb521ed29": "该文件处于冲突状态,但没有可编辑的工作树文件。", @@ -1250,7 +1251,17 @@ "noRunTargets": "此项目还没有可用的运行目标。", "perWorkspaceEnvHint": "通过环境模板按需创建环境", "branchName": "分支名称", - "branchNamePlaceholder": "feature/my-branch" + "branchNamePlaceholder": "feature/my-branch", + "connectTimedOut": "Connection timed out. It may still be connecting in the background.", + "connectingHost": "Connecting…", + "connectHost": "Connect", + "addHost": "Add host", + "addHostHint": "Register another machine or Orca server", + "addSshHost": "Add SSH host", + "addSshHostHint": "Use an existing machine over SSH", + "addRemoteOrcaServer": "Add Remote Orca Server", + "addRemoteOrcaServerHint": "Pair another Orca runtime", + "hostConnectionFailed": "Connection failed" }, "NewWorkspaceComposerModal": { "createWorktree": "创建工作树", @@ -4404,7 +4415,23 @@ "0bed8727db": "它可能已被手机或删除。刷新工作区或将其从 Orca 中删除。", "3921d3d9a5": "找不到工作区文件夹。", "f387af445b": "工作区路径不是有效的本地路径。", - "3ec372b664": "文件管理器" + "3ec372b664": "文件管理器", + "localOnly": "Local only", + "remoteSsh": "Remote SSH", + "remoteRuntimeUnsupported": "Opening this path in a local app is not available.", + "remoteRuntimeUnsupportedDetail": "Switch to a local or SSH workspace, then try again.", + "sshTargetNotFound": "SSH host is no longer available.", + "sshTargetNotFoundDetail": "Refresh workspaces or reconnect the host, then try again.", + "sshTargetInvalid": "SSH host configuration is incomplete.", + "sshTargetInvalidDetail": "Edit or reconnect the SSH host, then try again.", + "sshAliasRequired": "VS Code needs an SSH config alias for this host.", + "sshAliasRequiredDetail": "Add a Host alias for {{host}}:{{port}} to your local SSH config, reconnect the workspace, then try again.", + "remoteEditorUnsupported": "This app cannot open SSH workspaces.", + "remoteEditorUnsupportedDetail": "Choose VS Code or use the app locally.", + "remotePathInvalid": "Path is not valid for the SSH host.", + "remotePathInvalidDetail": "Refresh the workspace before trying again.", + "remoteLaunchFailed": "Could not open the path in VS Code.", + "remoteLaunchFailedDetail": "Check the VS Code command configured on this machine." }, "WorktreeTitleInlineRename": { "2f42ae024f": "未读:", @@ -4500,7 +4527,14 @@ "worktree": { "flow": { "c460fecc4a": "无法睡眠某些工作区", - "8bc3fc0671": "无法睡眠工作区" + "8bc3fc0671": "无法睡眠工作区", + "legacy": { + "unverified": "The older host runtime could not confirm terminal shutdown. The workspace was kept open; update the host and try again." + }, + "host": { + "unverified": "The host could not confirm terminal shutdown. The workspace was kept open; check the connection and try again." + }, + "retry": "The workspace was kept open. Try again; if the problem continues, check the host connection." } } }, @@ -4809,7 +4843,8 @@ "sshImportFailed": "导入 SSH 配置失败。", "importing": "正在导入...", "importSshConfig": "导入 ~/.ssh/config", - "sshPersistenceDefault": "此主机上的远程终端会保持运行,直到你结束它们或重置中继。" + "sshPersistenceDefault": "此主机上的远程终端会保持运行,直到你结束它们或重置中继。", + "advanced": "Advanced" }, "ForgetSshWorkspaceDialog": { "reconnectFailed": "重新连接失败", @@ -6422,7 +6457,9 @@ "versionUnavailable": "Orca version unavailable", "updateServer": "Update", "reviewServerUpdates": "Server updates", - "orcaVersion": "Orca v{{value0}}" + "orcaVersion": "Orca v{{value0}}", + "removeActiveServerBlocked": "Choose another Active Server in Advanced before removing this server.", + "removeActiveServerDescription": "Choose another Active Server in Advanced before removing this server. Existing host sessions are left alone." }, "RuntimePairingGeneratedUrlRows": { "0495f68959": "复制 {{value0}}" diff --git a/src/renderer/src/runtime/web-session-tabs-sync.ts b/src/renderer/src/runtime/web-session-tabs-sync.ts index ef1b15075df..969bacc8b4b 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync.ts +++ b/src/renderer/src/runtime/web-session-tabs-sync.ts @@ -2769,16 +2769,22 @@ export function useWebSessionTabsSync(): void { environmentId ) ) - ).then((recovered) => { - if (!disposed) { - const applicable = recovered.filter( - (snapshot): snapshot is RuntimeMobileSessionTabsResult => snapshot !== null - ) - applyWebSessionTabsStorePatch((state) => - applyFreshWebSessionTabsSnapshots(state, applicable, environmentId) - ) - } - }) + ) + .then((recovered) => { + if (!disposed) { + const applicable = recovered.filter( + (snapshot): snapshot is RuntimeMobileSessionTabsResult => snapshot !== null + ) + applyWebSessionTabsStorePatch((state) => + applyFreshWebSessionTabsSnapshots(state, applicable, environmentId) + ) + } + }) + .catch((error) => { + if (!disposed) { + console.warn('[web-session-tabs-sync] snapshot recovery failed:', error) + } + }) return } if (event.type !== 'snapshot' && event.type !== 'updated') { @@ -2791,13 +2797,19 @@ export function useWebSessionTabsSync(): void { useAppStore.getState(), event, environmentId - ).then((recovered) => { - if (!disposed && recovered) { - applyWebSessionTabsStorePatch((state) => - applyFreshWebSessionTabsSnapshot(state, recovered, environmentId) - ) - } - }) + ) + .then((recovered) => { + if (!disposed && recovered) { + applyWebSessionTabsStorePatch((state) => + applyFreshWebSessionTabsSnapshot(state, recovered, environmentId) + ) + } + }) + .catch((error) => { + if (!disposed) { + console.warn('[web-session-tabs-sync] snapshot recovery failed:', error) + } + }) }, onError: (error) => { console.warn('[web-session-tabs-sync] global subscription error:', error.message) @@ -2907,7 +2919,7 @@ export function useWebSessionTabsSync(): void { } if (!disposed && shouldBootstrapInitialTerminal) { requestedInitialTerminal = true - void createWebRuntimeSessionTerminal({ + await createWebRuntimeSessionTerminal({ worktreeId: activeWorktreeId, environmentId, activate: true @@ -2918,14 +2930,12 @@ export function useWebSessionTabsSync(): void { beginWebRuntimeWakeTerminalRespawn(activeWorktreeId) ) { requestedRespawnAfterWake = true - void createWebRuntimeSessionTerminal({ + await createWebRuntimeSessionTerminal({ worktreeId: activeWorktreeId, environmentId, activate: true, selectWorktree: false - }).finally(() => { - endWebRuntimeWakeTerminalRespawn(activeWorktreeId) - }) + }).finally(() => endWebRuntimeWakeTerminalRespawn(activeWorktreeId)) } } void window.api.runtimeEnvironments @@ -2953,7 +2963,11 @@ export function useWebSessionTabsSync(): void { if (event.type !== 'snapshot' && event.type !== 'updated') { return } - void applyActiveSnapshot(event, response) + void applyActiveSnapshot(event, response).catch((error) => { + if (!disposed) { + console.warn('[web-session-tabs-sync] active snapshot recovery failed:', error) + } + }) }, onError: (error) => { console.warn('[web-session-tabs-sync] subscription error:', error.message) diff --git a/src/renderer/src/store/slices/browser.ts b/src/renderer/src/store/slices/browser.ts index 2a322e0f6eb..6afcb24a840 100644 --- a/src/renderer/src/store/slices/browser.ts +++ b/src/renderer/src/store/slices/browser.ts @@ -1924,7 +1924,10 @@ export const createBrowserSlice: StateCreator = importCookiesToProfile: async (profileId) => { const hostId = getBrowserSettingsHostId(get()) if (getBrowserSettingsRuntimeEnvironmentId(get())) { - const reason = 'Manual cookie file import is unavailable while a remote runtime is active.' + const reason = translate( + 'auto.store.slices.browser.remoteCookieImportUnavailable', + 'Manual cookie file import is unavailable while a remote runtime is active.' + ) set((state) => browserImportStateForHostUpdate(state, hostId, { profileId, diff --git a/src/shared/runtime-types.ts b/src/shared/runtime-types.ts index 42d017b29cc..c90762c65e8 100644 --- a/src/shared/runtime-types.ts +++ b/src/shared/runtime-types.ts @@ -39,6 +39,7 @@ import type { import type { StartupCommandDelivery } from './codex-startup-delivery' import type { RemoteServerUpdateSupport } from './remote-server-update' import type { ExecutionHostId } from './execution-host' +import type { PtyIncarnationId } from './pty-incarnation' export type { RuntimeMarkdownReadTabResult, RuntimeMarkdownSaveTabResult } @@ -490,7 +491,7 @@ export type RuntimeTerminalListResult = { export type RuntimeTerminalOrphanAdoptionClaim = { terminal: string ptyId: string - incarnationId: string + incarnationId: PtyIncarnationId tabId: string leafId: string }