Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions src/web-ui/src/flow_chat/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -1993,9 +1993,12 @@ export const ChatInput: React.FC<ChatInputProps> = ({
* 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;

Expand Down Expand Up @@ -2031,6 +2034,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
derivedState?.isProcessing,
isAcpTargetSession,
isSubagentInputTarget,
remoteWorkspaceSession,
tWorktrees,
usesDispatchTransport,
]);
Expand Down Expand Up @@ -2061,18 +2065,16 @@ export const ChatInput: React.FC<ChatInputProps> = ({
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 ||
Expand All @@ -2083,13 +2085,12 @@ export const ChatInput: React.FC<ChatInputProps> = ({
derivedState?.isProcessing,
effectiveTargetSession?.config.dispatchTarget,
effectiveTargetSession?.dialogTurns.length,
effectiveTargetSession?.remoteConnectionId,
handleSelectDispatchTarget,
isAcpInputSession,
isBtwSession,
isSubagentInputTarget,
registration,
workspace,
remoteWorkspaceSession,
workspacePath,
]);

Expand Down
23 changes: 23 additions & 0 deletions src/web-ui/src/flow_chat/utils/sessionWorkspace.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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',
Expand Down
16 changes: 16 additions & 0 deletions src/web-ui/src/flow_chat/utils/sessionWorkspace.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
import { isRemoteWorkspace, type WorkspaceInfo } from '@/shared/types';
import { isRemoteTraceContext } from '@/shared/utils/startupTrace';
import type { Session } from '../types/flow-chat';

type SessionWorkspaceBinding = Pick<
Session,
'workspacePath' | 'projectWorkspacePath' | 'config'
>;

/** Whether local-only workspace controls should be unavailable for this session. */
export function isRemoteWorkspaceSession(
session: Partial<Pick<Session, 'remoteConnectionId' | 'remoteSshHost' | 'config'>> | 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,
Expand Down
Loading