Skip to content

Commit f09ce19

Browse files
committed
feat: warn when concurrent local workspaces are streaming
Add ConcurrentLocalWarning component that displays a dismissible warning when: 1. Current workspace uses local project-dir runtime (no isolation) 2. Another workspace for the same project is actively streaming The warning helps users understand that agents may interfere with each other when working on the same directory without worktree isolation. Also adds projectPath prop to AIView to enable this check.
1 parent c34e725 commit f09ce19

File tree

3 files changed

+103
-0
lines changed

3 files changed

+103
-0
lines changed

src/browser/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,7 @@ function AppInner() {
565565
<AIView
566566
key={selectedWorkspace.workspaceId}
567567
workspaceId={selectedWorkspace.workspaceId}
568+
projectPath={selectedWorkspace.projectPath}
568569
projectName={selectedWorkspace.projectName}
569570
branch={
570571
selectedWorkspace.namedWorkspacePath?.split("/").pop() ??

src/browser/components/AIView.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { useAIViewKeybinds } from "@/browser/hooks/useAIViewKeybinds";
3636
import { evictModelFromLRU } from "@/browser/hooks/useModelLRU";
3737
import { QueuedMessage } from "./Messages/QueuedMessage";
3838
import { CompactionWarning } from "./CompactionWarning";
39+
import { ConcurrentLocalWarning } from "./ConcurrentLocalWarning";
3940
import { checkAutoCompaction } from "@/browser/utils/compaction/autoCompactionCheck";
4041
import { executeCompaction } from "@/browser/utils/chatCommands";
4142
import { useProviderOptions } from "@/browser/hooks/useProviderOptions";
@@ -44,6 +45,7 @@ import { useSendMessageOptions } from "@/browser/hooks/useSendMessageOptions";
4445

4546
interface AIViewProps {
4647
workspaceId: string;
48+
projectPath: string;
4749
projectName: string;
4850
branch: string;
4951
namedWorkspacePath: string; // User-friendly path for display and terminal
@@ -55,6 +57,7 @@ interface AIViewProps {
5557

5658
const AIViewInner: React.FC<AIViewProps> = ({
5759
workspaceId,
60+
projectPath,
5861
projectName,
5962
branch,
6063
namedWorkspacePath,
@@ -580,6 +583,11 @@ const AIViewInner: React.FC<AIViewProps> = ({
580583
onCompactClick={handleCompactClick}
581584
/>
582585
)}
586+
<ConcurrentLocalWarning
587+
workspaceId={workspaceId}
588+
projectPath={projectPath}
589+
runtimeConfig={runtimeConfig}
590+
/>
583591
<ChatInput
584592
variant="workspace"
585593
workspaceId={workspaceId}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React, { useState, useMemo } from "react";
2+
import { useWorkspaceContext } from "@/browser/contexts/WorkspaceContext";
3+
import { useWorkspaceStoreRaw } from "@/browser/stores/WorkspaceStore";
4+
import { isLocalProjectRuntime } from "@/common/types/runtime";
5+
import type { RuntimeConfig } from "@/common/types/runtime";
6+
import { useSyncExternalStore } from "react";
7+
8+
/**
9+
* Warning shown when a local project-dir workspace has another workspace
10+
* for the same project that is currently streaming.
11+
*
12+
* This warns users that agents may interfere with each other when
13+
* working on the same directory without isolation.
14+
*/
15+
export const ConcurrentLocalWarning: React.FC<{
16+
workspaceId: string;
17+
projectPath: string;
18+
runtimeConfig?: RuntimeConfig;
19+
}> = (props) => {
20+
const [dismissed, setDismissed] = useState(false);
21+
22+
// Only show for local project-dir runtimes (not worktree or SSH)
23+
const isLocalProject = isLocalProjectRuntime(props.runtimeConfig);
24+
25+
const { workspaceMetadata } = useWorkspaceContext();
26+
const store = useWorkspaceStoreRaw();
27+
28+
// Find other local project-dir workspaces for the same project
29+
const otherLocalWorkspaceIds = useMemo(() => {
30+
if (!isLocalProject) return [];
31+
32+
const result: string[] = [];
33+
for (const [id, meta] of workspaceMetadata) {
34+
// Skip current workspace
35+
if (id === props.workspaceId) continue;
36+
// Must be same project
37+
if (meta.projectPath !== props.projectPath) continue;
38+
// Must also be local project-dir runtime
39+
if (!isLocalProjectRuntime(meta.runtimeConfig)) continue;
40+
result.push(id);
41+
}
42+
return result;
43+
}, [isLocalProject, workspaceMetadata, props.workspaceId, props.projectPath]);
44+
45+
// Subscribe to streaming state of other local workspaces
46+
// We need to check if any of them have canInterrupt === true
47+
const streamingWorkspaceName = useSyncExternalStore(
48+
(listener) => {
49+
// Subscribe to all other local workspaces
50+
const unsubscribers = otherLocalWorkspaceIds.map((id) => store.subscribeKey(id, listener));
51+
return () => unsubscribers.forEach((unsub) => unsub());
52+
},
53+
() => {
54+
// Find first streaming workspace
55+
for (const id of otherLocalWorkspaceIds) {
56+
try {
57+
const state = store.getWorkspaceSidebarState(id);
58+
if (state.canInterrupt) {
59+
const meta = workspaceMetadata.get(id);
60+
return meta?.name ?? id;
61+
}
62+
} catch {
63+
// Workspace may not be registered yet, skip
64+
}
65+
}
66+
return null;
67+
}
68+
);
69+
70+
// Don't show if:
71+
// - Not a local project-dir runtime
72+
// - No other local workspaces are streaming
73+
// - User dismissed the warning
74+
if (!isLocalProject || !streamingWorkspaceName || dismissed) {
75+
return null;
76+
}
77+
78+
return (
79+
<div className="mx-4 mt-2 mb-1 flex items-center justify-between gap-2 rounded bg-yellow-900/30 px-3 py-1.5 text-xs text-yellow-200">
80+
<span>
81+
<strong>{streamingWorkspaceName}</strong> is also running in this project. Agents may
82+
interfere — consider using only one at a time.
83+
</span>
84+
<button
85+
type="button"
86+
onClick={() => setDismissed(true)}
87+
className="shrink-0 text-yellow-400 hover:text-yellow-200"
88+
title="Dismiss warning"
89+
>
90+
91+
</button>
92+
</div>
93+
);
94+
};

0 commit comments

Comments
 (0)