diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index 05bf58e374..87350fe7d3 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -47,7 +47,6 @@ import { type SlashActionId, } from '../utils/slashActionSelection'; import { notificationService } from '@/shared/notification-system'; -import { isRemoteWorkspace } from '@/shared/types'; import { useI18n } from '@/infrastructure/i18n'; import { inputReducer, initialInputState, type InputAction } from '../reducers/inputReducer'; import { modeReducer, initialModeState } from '../reducers/modeReducer'; @@ -91,6 +90,7 @@ import { isSessionWorktreeBindingLocked, sessionWorktreeBindingSubscriptionKey, } from '../utils/sessionWorktree'; +import { isRemoteWorkspaceSession } from '../utils/sessionWorkspace'; import { isTauriRuntime } from '@/infrastructure/runtime'; import { Tooltip, IconButton, confirmDanger, confirmWarning } from '@/component-library'; import { PendingQueuePanel } from './PendingQueuePanel'; @@ -1993,9 +1993,12 @@ export const ChatInput: React.FC = ({ * Checking worktree isolation only arms the empty session. The first prompt * materializes the worktree after it has visibly been submitted. */ + const remoteWorkspaceSession = + isRemoteWorkspaceSession(effectiveTargetSession, workspace); + const worktreeControl = useMemo(() => { if (!effectiveTargetSessionId || !effectiveTargetSession) return undefined; - if (effectiveTargetSession.remoteConnectionId) return undefined; + if (remoteWorkspaceSession) return undefined; if (usesDispatchTransport) return undefined; if (isSubagentInputTarget || isAcpTargetSession) return undefined; @@ -2031,6 +2034,7 @@ export const ChatInput: React.FC = ({ derivedState?.isProcessing, isAcpTargetSession, isSubagentInputTarget, + remoteWorkspaceSession, tWorktrees, usesDispatchTransport, ]); @@ -2061,18 +2065,16 @@ export const ChatInput: React.FC = ({ registration || isBtwSession || isSubagentInputTarget || - isAcpInputSession + isAcpInputSession || + remoteWorkspaceSession ) { return undefined; } const target: DispatchTarget = effectiveTargetSession?.config.dispatchTarget ?? { kind: 'local' }; - const snapshotSourceIsRemote = - !!effectiveTargetSession?.remoteConnectionId || isRemoteWorkspace(workspace); return { target, - sourceWorkspacePath: - !snapshotSourceIsRemote && workspacePath ? workspacePath : undefined, + sourceWorkspacePath: workspacePath || undefined, locked: isNonLocalDispatchTarget(target) || (effectiveTargetSession?.dialogTurns.length ?? 0) > 0 || @@ -2083,13 +2085,12 @@ export const ChatInput: React.FC = ({ derivedState?.isProcessing, effectiveTargetSession?.config.dispatchTarget, effectiveTargetSession?.dialogTurns.length, - effectiveTargetSession?.remoteConnectionId, handleSelectDispatchTarget, isAcpInputSession, isBtwSession, isSubagentInputTarget, registration, - workspace, + remoteWorkspaceSession, workspacePath, ]); diff --git a/src/web-ui/src/flow_chat/utils/sessionWorkspace.test.ts b/src/web-ui/src/flow_chat/utils/sessionWorkspace.test.ts index e6df1ecbb7..778bfe9dc6 100644 --- a/src/web-ui/src/flow_chat/utils/sessionWorkspace.test.ts +++ b/src/web-ui/src/flow_chat/utils/sessionWorkspace.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest'; +import { WorkspaceKind, type WorkspaceInfo } from '@/shared/types'; import type { Session } from '../types/flow-chat'; import { + isRemoteWorkspaceSession, requireSessionProjectWorkspacePath, sessionExecutionWorkspacePath, sessionProjectWorkspacePath, @@ -18,6 +20,27 @@ function session( } describe('sessionWorkspace', () => { + it('identifies remote workspace sessions from either session or workspace metadata', () => { + const localWorkspace = { + workspaceKind: WorkspaceKind.Normal, + } as WorkspaceInfo; + const remoteWorkspace = { + workspaceKind: WorkspaceKind.Remote, + } as WorkspaceInfo; + + expect(isRemoteWorkspaceSession(undefined, localWorkspace)).toBe(false); + expect( + isRemoteWorkspaceSession({ remoteConnectionId: 'remote-1' }, localWorkspace), + ).toBe(true); + expect( + isRemoteWorkspaceSession( + { config: { remoteConnectionId: 'remote-2' } }, + localWorkspace, + ), + ).toBe(true); + expect(isRemoteWorkspaceSession(undefined, remoteWorkspace)).toBe(true); + }); + it('keeps execution and project roots distinct for a worktree session', () => { const worktreeSession = session({ workspacePath: '/worktrees/wt-1', diff --git a/src/web-ui/src/flow_chat/utils/sessionWorkspace.ts b/src/web-ui/src/flow_chat/utils/sessionWorkspace.ts index 1630b40351..7d099c4518 100644 --- a/src/web-ui/src/flow_chat/utils/sessionWorkspace.ts +++ b/src/web-ui/src/flow_chat/utils/sessionWorkspace.ts @@ -1,3 +1,5 @@ +import { isRemoteWorkspace, type WorkspaceInfo } from '@/shared/types'; +import { isRemoteTraceContext } from '@/shared/utils/startupTrace'; import type { Session } from '../types/flow-chat'; type SessionWorkspaceBinding = Pick< @@ -5,6 +7,20 @@ type SessionWorkspaceBinding = Pick< 'workspacePath' | 'projectWorkspacePath' | 'config' >; +/** Whether local-only workspace controls should be unavailable for this session. */ +export function isRemoteWorkspaceSession( + session: Partial> | undefined, + workspace: WorkspaceInfo | null | undefined, +): boolean { + return ( + isRemoteTraceContext( + session?.remoteConnectionId || session?.config?.remoteConnectionId, + session?.remoteSshHost || session?.config?.remoteSshHost, + ) + || isRemoteWorkspace(workspace) + ); +} + /** Concrete root in which terminal, Git, and file tools execute. */ export function sessionExecutionWorkspacePath( session: SessionWorkspaceBinding,