diff --git a/src/renderer/src/components/settings/RuntimeEnvironmentsPane.test.ts b/src/renderer/src/components/settings/RuntimeEnvironmentsPane.test.ts index c3e1db6e295..8c001f95086 100644 --- a/src/renderer/src/components/settings/RuntimeEnvironmentsPane.test.ts +++ b/src/renderer/src/components/settings/RuntimeEnvironmentsPane.test.ts @@ -14,6 +14,7 @@ import { getHostModelCapabilitySummary, getRuntimeCapabilitiesSummary, getRuntimeServerConnectionState, + isRuntimeEnvironmentRemovalBlocked, type RuntimeHostDetails } from './RuntimeEnvironmentsPane' @@ -208,4 +209,10 @@ describe('RuntimeEnvironmentsPane host details', () => { expect(getActiveServerModeDescription(false)).toContain('default Host') expect(getActiveServerModeDescription(false)).toContain('paired Orca runtime') }) + + it('blocks removing the active server independently of local-runtime availability', () => { + expect(isRuntimeEnvironmentRemovalBlocked('windows-2', 'windows-2')).toBe(true) + expect(isRuntimeEnvironmentRemovalBlocked(undefined, 'windows-2')).toBe(false) + expect(isRuntimeEnvironmentRemovalBlocked('local', 'windows-2')).toBe(false) + }) }) diff --git a/src/renderer/src/components/settings/RuntimeEnvironmentsPane.tsx b/src/renderer/src/components/settings/RuntimeEnvironmentsPane.tsx index 488704193d2..a2237d5a551 100644 --- a/src/renderer/src/components/settings/RuntimeEnvironmentsPane.tsx +++ b/src/renderer/src/components/settings/RuntimeEnvironmentsPane.tsx @@ -200,6 +200,13 @@ export function getActiveServerModeDescription(allowLocalRuntime: boolean): stri ) } +export function isRuntimeEnvironmentRemovalBlocked( + activeRuntimeEnvironmentId: string | null | undefined, + environmentId: string +): boolean { + return activeRuntimeEnvironmentId === environmentId +} + type RuntimeServerConnectionState = 'connected' | 'checking' | 'disconnected' export function getRuntimeServerConnectionState( @@ -294,7 +301,9 @@ export function RuntimeEnvironmentsPane({ switchingValue !== null || removingId !== null || disconnectingId !== null - const removingActiveServer = pendingRemove?.id === settings.activeRuntimeEnvironmentId + const removingActiveServer = pendingRemove + ? isRuntimeEnvironmentRemovalBlocked(settings.activeRuntimeEnvironmentId, pendingRemove.id) + : false const searchEntry = canGeneratePairingUrl ? getRuntimeEnvironmentsSearchEntry() : getWebRuntimeEnvironmentsSearchEntry() @@ -510,7 +519,7 @@ export function RuntimeEnvironmentsPane({ setRemovingId(environment.id) setRemoveError(null) try { - if (allowLocalRuntime && settings.activeRuntimeEnvironmentId === environment.id) { + if (isRuntimeEnvironmentRemovalBlocked(settings.activeRuntimeEnvironmentId, environment.id)) { if (mountedRef.current) { setRemoveError( translate( @@ -1385,15 +1394,10 @@ export function RuntimeEnvironmentsPane({ {removingActiveServer - ? allowLocalRuntime - ? translate( - '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', - 'Removing the active server disconnects this browser from that host. Existing host sessions are left alone.' - ) + ? translate( + '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.ed3e3f069d', 'This removes the saved server from Orca. It does not change the active server.' diff --git a/src/renderer/src/hooks/useIpcEvents.test.ts b/src/renderer/src/hooks/useIpcEvents.test.ts index 61d4bf65bf2..cf1a370abf6 100644 --- a/src/renderer/src/hooks/useIpcEvents.test.ts +++ b/src/renderer/src/hooks/useIpcEvents.test.ts @@ -1825,7 +1825,12 @@ describe('useIpcEvents updater integration', () => { folderWorkspaces: [], projectGroups: [], repos: [{ id: 'repo-1', connectionId: null, executionHostId: 'local' }], - worktreesByRepo: { 'repo-1': [{ id: 'wt-2', repoId: 'repo-1' }] }, + worktreesByRepo: { + 'repo-1': ['wt-1', 'wt-2', 'wt-3', 'wt-4', 'wt-history'].map((id) => ({ + id, + repoId: 'repo-1' + })) + } as Record, openFiles: [], browserTabsByWorktree: {}, tabBarOrderByWorktree: {}, @@ -2444,12 +2449,18 @@ describe('useIpcEvents updater integration', () => { createTab.mockClear() replyTerminalCreate.mockClear() - storeState.repos.push({ - id: 'repo-remote', - connectionId: null, - executionHostId: 'runtime:focused-runtime' - }) - storeState.worktreesByRepo['repo-remote'] = [{ id: 'wt-remote', repoId: 'repo-remote' }] + storeState.repos = [ + ...storeState.repos, + { + id: 'repo-remote', + connectionId: null, + executionHostId: 'runtime:focused-runtime' + } + ] + storeState.worktreesByRepo = { + ...storeState.worktreesByRepo, + 'repo-remote': [{ id: 'wt-remote', repoId: 'repo-remote' }] + } requestTerminalCreateListenerRef.current({ requestId: 'req-remote-owner-blocked', worktreeId: 'wt-remote', @@ -2460,6 +2471,53 @@ describe('useIpcEvents updater integration', () => { requestId: 'req-remote-owner-blocked', error: 'Local terminal creation is unavailable while a remote runtime is active' }) + delete storeState.worktreesByRepo['repo-remote'] + storeState.repos = storeState.repos.filter((repo) => repo.id !== 'repo-remote') + + createTab.mockClear() + replyTerminalCreate.mockClear() + storeState.repos = [ + ...storeState.repos, + { + id: 'repo-conflicting-owner', + connectionId: null, + executionHostId: 'runtime:focused-runtime' + } + ] + storeState.worktreesByRepo = { + ...storeState.worktreesByRepo, + 'repo-1': [...storeState.worktreesByRepo['repo-1'], { id: 'wt-ambiguous', repoId: 'repo-1' }], + 'repo-conflicting-owner': [{ id: 'wt-ambiguous', repoId: 'repo-conflicting-owner' }] + } + requestTerminalCreateListenerRef.current({ + requestId: 'req-ambiguous-owner', + worktreeId: 'wt-ambiguous', + title: 'Ambiguous Terminal', + source: 'runtime-session' + }) + expect(createTab).not.toHaveBeenCalled() + expect(replyTerminalCreate).toHaveBeenCalledWith({ + requestId: 'req-ambiguous-owner', + error: 'Terminal creation is unavailable because the worktree owner could not be resolved' + }) + storeState.worktreesByRepo['repo-1'] = storeState.worktreesByRepo['repo-1'].filter( + (worktree) => worktree.id !== 'wt-ambiguous' + ) + delete storeState.worktreesByRepo['repo-conflicting-owner'] + storeState.repos = storeState.repos.filter((repo) => repo.id !== 'repo-conflicting-owner') + + createTab.mockClear() + replyTerminalCreate.mockClear() + requestTerminalCreateListenerRef.current({ + requestId: 'req-missing-owner', + worktreeId: 'wt-missing', + title: 'Missing Terminal' + }) + expect(createTab).not.toHaveBeenCalled() + expect(replyTerminalCreate).toHaveBeenCalledWith({ + requestId: 'req-missing-owner', + error: 'Terminal creation is unavailable because the worktree owner could not be resolved' + }) storeState.settings.activeRuntimeEnvironmentId = undefined if (typeof focusTerminalListenerRef.current !== 'function') { diff --git a/src/renderer/src/hooks/useIpcEvents.ts b/src/renderer/src/hooks/useIpcEvents.ts index 237be326206..b31d4068ea6 100644 --- a/src/renderer/src/hooks/useIpcEvents.ts +++ b/src/renderer/src/hooks/useIpcEvents.ts @@ -133,6 +133,7 @@ import { showTerminalShortcutCaptureNotification } from '@/lib/terminal-shortcut import { resolveAgentStatusTerminalTitle } from '@/lib/agent-status-terminal-title' import { titleHasAgentName } from '../../../shared/agent-detection' import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' +import { resolveTerminalWorktreeRoute } from '@/lib/terminal-worktree-route' import { resolveAgentPaneAuthorityKey } from '@/store/slices/agent-pane-authority' import { translate } from '@/i18n/i18n' import { closeTerminalTab } from '@/components/terminal/terminal-tab-actions' @@ -1707,11 +1708,19 @@ export function useIpcEvents(): void { }) return } + const worktreeRoute = resolveTerminalWorktreeRoute(store, worktreeId) + if (!worktreeRoute) { + window.api.ui.replyTerminalCreate({ + requestId: data.requestId, + error: translate( + 'auto.hooks.useIpcEvents.unresolvedTerminalWorktreeOwner', + 'Terminal creation is unavailable because the worktree owner could not be resolved' + ) + }) + return + } // Why: runtime-session requests are host-owned tabs materialized by this renderer, not ordinary local creates. - if ( - getRuntimeEnvironmentIdForWorktree(store, worktreeId) && - data.source !== 'runtime-session' - ) { + if (worktreeRoute.runtimeEnvironmentId && data.source !== 'runtime-session') { window.api.ui.replyTerminalCreate({ requestId: data.requestId, error: translate( diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index d71288e7a8c..7dab7caa569 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -684,7 +684,8 @@ "4f78ba5885": "Workspace synced", "ef223fbb6b": "A device tried to connect but is not paired", "11992d0337": "If this was your phone or another Orca client, re-pair it from Settings → Mobile.", - "6573cfe955": "Open Mobile Settings" + "6573cfe955": "Open Mobile Settings", + "unresolvedTerminalWorktreeOwner": "Terminal creation is unavailable because the worktree owner could not be resolved" }, "useSettingsNavigationMetadata": { "4a728cd56b": "New features that are still taking shape. Give them a try.", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 04ee99a8aa3..c3f28a39a3f 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -661,7 +661,8 @@ "4f78ba5885": "Espacio de trabajo sincronizado", "ef223fbb6b": "A device tried to connect but is not paired", "11992d0337": "If this was your phone or another Orca client, re-pair it from Settings → Mobile.", - "6573cfe955": "Open Mobile Settings" + "6573cfe955": "Open Mobile Settings", + "unresolvedTerminalWorktreeOwner": "Terminal creation is unavailable because the worktree owner could not be resolved" }, "useSettingsNavigationMetadata": { "4a728cd56b": "Nuevas funciones que aún están tomando forma. Pruébalas.", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 98a05973f74..a3d9b8b8cf2 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -661,7 +661,8 @@ "4f78ba5885": "ワークスペースが同期されました", "ef223fbb6b": "A device tried to connect but is not paired", "11992d0337": "If this was your phone or another Orca client, re-pair it from Settings → Mobile.", - "6573cfe955": "Open Mobile Settings" + "6573cfe955": "Open Mobile Settings", + "unresolvedTerminalWorktreeOwner": "Terminal creation is unavailable because the worktree owner could not be resolved" }, "useSettingsNavigationMetadata": { "4a728cd56b": "まだ形になりつつある新機能。試してみてください。", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 0ef838f562c..03fd20804b7 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -661,7 +661,8 @@ "4f78ba5885": "워크스페이스가 동기화되었습니다.", "ef223fbb6b": "A device tried to connect but is not paired", "11992d0337": "If this was your phone or another Orca client, re-pair it from Settings → Mobile.", - "6573cfe955": "Open Mobile Settings" + "6573cfe955": "Open Mobile Settings", + "unresolvedTerminalWorktreeOwner": "Terminal creation is unavailable because the worktree owner could not be resolved" }, "useSettingsNavigationMetadata": { "4a728cd56b": "아직 구체화되고 있는 새로운 기능입니다. 한번 시도해 보세요.", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 6f9b524eb3b..f16fd574905 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -661,7 +661,8 @@ "4f78ba5885": "工作区已同步", "ef223fbb6b": "A device tried to connect but is not paired", "11992d0337": "If this was your phone or another Orca client, re-pair it from Settings → Mobile.", - "6573cfe955": "Open Mobile Settings" + "6573cfe955": "Open Mobile Settings", + "unresolvedTerminalWorktreeOwner": "Terminal creation is unavailable because the worktree owner could not be resolved" }, "useSettingsNavigationMetadata": { "4a728cd56b": "仍在完善中的新功能。尝试一下。",