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
2 changes: 1 addition & 1 deletion src/renderer/App/hooks/useFocusSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function useFocusSession({ onSwitchWorktree, onSwitchTab }: UseFocusSessi
if (session) {
// Switch to the session's worktree first, then set active session (same as RunningProjectsPopover)
onSwitchWorktree(session.cwd);
useAgentSessionsStore.getState().setActiveId(session.cwd, sessionId);
useAgentSessionsStore.getState().setActiveId(session.repoPath, session.cwd, sessionId);
onSwitchTab('chat');
}
});
Expand Down
26 changes: 14 additions & 12 deletions src/renderer/components/chat/AgentPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -452,9 +452,8 @@ export function AgentPanel({ repoPath, cwd, isActive = false, onSwitchWorktree }
useEffect(() => {
if (!cwd || groups.length === 0) return;

const normalizedCwd = normalizePath(cwd);
const unsubscribe = useAgentSessionsStore.subscribe(
(state) => state.activeIds[normalizedCwd],
(state) => state.activeIds[`${normalizePath(repoPath)}::${normalizePath(cwd)}`],
(storeActiveId) => {
if (!storeActiveId) return;

Expand All @@ -476,7 +475,7 @@ export function AgentPanel({ repoPath, cwd, isActive = false, onSwitchWorktree }
);

return unsubscribe;
}, [cwd, groups, updateCurrentGroupState]);
}, [repoPath, cwd, groups, updateCurrentGroupState]);

// Empty state agent menu
const [showAgentMenu, setShowAgentMenu] = useState(false);
Expand Down Expand Up @@ -607,7 +606,7 @@ export function AgentPanel({ repoPath, cwd, isActive = false, onSwitchWorktree }
};
});

setActiveId(cwd, newSession.id);
setActiveId(repoPath, cwd, newSession.id);
clearContinueRequest();
}
}, [
Expand Down Expand Up @@ -791,7 +790,7 @@ export function AgentPanel({ repoPath, cwd, isActive = false, onSwitchWorktree }
// Handle session selection
const handleSelectSession = useCallback(
(id: string, groupId?: string) => {
setActiveId(cwd, id);
setActiveId(repoPath, cwd, id);

updateCurrentGroupState((state) => {
const targetGroupId = groupId || state.groups.find((g) => g.sessionIds.includes(id))?.id;
Expand All @@ -806,7 +805,7 @@ export function AgentPanel({ repoPath, cwd, isActive = false, onSwitchWorktree }
};
});
},
[cwd, setActiveId, updateCurrentGroupState]
[cwd, repoPath, setActiveId, updateCurrentGroupState]
);

// Notification payload may carry either UI session id or Claude sessionId.
Expand Down Expand Up @@ -942,14 +941,14 @@ export function AgentPanel({ repoPath, cwd, isActive = false, onSwitchWorktree }
const nextIndex = (currentIndex + 1) % activeGroup.sessionIds.length;
const nextSessionId = activeGroup.sessionIds[nextIndex];

setActiveId(cwd, nextSessionId);
setActiveId(repoPath, cwd, nextSessionId);
updateCurrentGroupState((state) => ({
...state,
groups: state.groups.map((g) =>
g.id === activeGroupId ? { ...g, activeSessionId: nextSessionId } : g
),
}));
}, [groups, activeGroupId, cwd, setActiveId, updateCurrentGroupState]);
}, [groups, activeGroupId, cwd, repoPath, setActiveId, updateCurrentGroupState]);

const handlePrevSession = useCallback(() => {
const activeGroup = groups.find((g) => g.id === activeGroupId);
Expand All @@ -959,14 +958,14 @@ export function AgentPanel({ repoPath, cwd, isActive = false, onSwitchWorktree }
const prevIndex = currentIndex <= 0 ? activeGroup.sessionIds.length - 1 : currentIndex - 1;
const prevSessionId = activeGroup.sessionIds[prevIndex];

setActiveId(cwd, prevSessionId);
setActiveId(repoPath, cwd, prevSessionId);
updateCurrentGroupState((state) => ({
...state,
groups: state.groups.map((g) =>
g.id === activeGroupId ? { ...g, activeSessionId: prevSessionId } : g
),
}));
}, [groups, activeGroupId, cwd, setActiveId, updateCurrentGroupState]);
}, [groups, activeGroupId, cwd, repoPath, setActiveId, updateCurrentGroupState]);

const handleInitialized = useCallback(
(id: string) => {
Expand Down Expand Up @@ -1340,7 +1339,10 @@ export function AgentPanel({ repoPath, cwd, isActive = false, onSwitchWorktree }

const normalizedCwd = normalizePath(cwd);
const currentState = worktreeGroupStates[normalizedCwd];
const storeActiveId = useAgentSessionsStore.getState().activeIds[normalizedCwd];
const storeActiveId =
useAgentSessionsStore.getState().activeIds[
`${normalizePath(repoPath)}::${normalizePath(cwd)}`
];

// If no groups exist but sessions do, create a group with all sessions
if (!currentState || currentState.groups.length === 0) {
Expand Down Expand Up @@ -1386,7 +1388,7 @@ export function AgentPanel({ repoPath, cwd, isActive = false, onSwitchWorktree }
}
}
}
}, [cwd, currentWorktreeSessions, worktreeGroupStates, setGroupState]);
}, [repoPath, cwd, currentWorktreeSessions, worktreeGroupStates, setGroupState]);

// Maintain global session IDs - include ALL sessions across all repos
// This ensures terminals stay mounted when switching between repos
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/layout/MainContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export function MainContent({
const activeIds = useAgentSessionsStore((s) => s.activeIds);
const activeSessionId = useMemo(() => {
if (!repoPath || !worktreePath) return null;
const key = normalizePath(worktreePath);
const key = `${normalizePath(repoPath)}::${normalizePath(worktreePath)}`;
const activeId = activeIds[key];
if (activeId) {
const session = sessions.find((s) => s.id === activeId);
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/layout/RunningProjectsPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export function RunningProjectsPopover({
break;
case 'agent':
await onSelectWorktreeByPath(item.session.cwd);
setAgentActiveId(item.session.cwd, item.session.id);
setAgentActiveId(item.session.repoPath, item.session.cwd, item.session.id);
onSwitchTab?.('chat');
break;
case 'terminal':
Expand Down
55 changes: 42 additions & 13 deletions src/renderer/stores/agentSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,21 @@ function isResumableAgent(agentCommand: string): boolean {
return agentCommand?.startsWith('claude') ?? false;
}

/**
* Composite key for activeIds: uniquely identifies a repo+worktree pair.
* Prevents cross-repo session pollution when different repos have worktrees
* with the same path name.
*/
function makeActiveKey(repoPath: string, cwd: string): string {
return `${normalizePath(repoPath)}::${normalizePath(cwd)}`;
}

// Group states indexed by normalized worktree path
type WorktreeGroupStates = Record<string, AgentGroupState>;

interface AgentSessionsState {
sessions: Session[];
activeIds: Record<string, string | null>; // key = cwd (worktree path)
activeIds: Record<string, string | null>; // key = makeActiveKey(repoPath, cwd)
groupStates: WorktreeGroupStates; // Group states per worktree (not persisted)
runtimeStates: Record<string, SessionRuntimeState>; // Runtime output states (not persisted)
enhancedInputStates: Record<string, EnhancedInputState>; // Enhanced input states per session (not persisted)
Expand All @@ -58,7 +67,7 @@ interface AgentSessionsState {
addSession: (session: Session) => void;
removeSession: (id: string) => void;
updateSession: (id: string, updates: Partial<Session>) => void;
setActiveId: (cwd: string, sessionId: string | null) => void;
setActiveId: (repoPath: string, cwd: string, sessionId: string | null) => void;
reorderSessions: (repoPath: string, cwd: string, fromIndex: number, toIndex: number) => void;
getSessions: (repoPath: string, cwd: string) => Session[];
getActiveSessionId: (repoPath: string, cwd: string) => string | null;
Expand Down Expand Up @@ -191,7 +200,10 @@ export const useAgentSessionsStore = create<AgentSessionsState>()(
const newSession = { ...session, displayOrder: maxOrder + 1 };
return {
sessions: [...state.sessions, newSession],
activeIds: { ...state.activeIds, [normalizePath(session.cwd)]: session.id },
activeIds: {
...state.activeIds,
[makeActiveKey(session.repoPath, session.cwd)]: session.id,
},
// Initialize enhanced input state for new session to ensure auto-popup works
enhancedInputStates: {
...state.enhancedInputStates,
Expand Down Expand Up @@ -241,9 +253,9 @@ export const useAgentSessionsStore = create<AgentSessionsState>()(
sessions: state.sessions.map((s) => (s.id === id ? { ...s, ...updates } : s)),
})),

setActiveId: (cwd, sessionId) =>
setActiveId: (repoPath, cwd, sessionId) =>
set((state) => ({
activeIds: { ...state.activeIds, [normalizePath(cwd)]: sessionId },
activeIds: { ...state.activeIds, [makeActiveKey(repoPath, cwd)]: sessionId },
})),

reorderSessions: (repoPath, cwd, fromIndex, toIndex) =>
Expand Down Expand Up @@ -289,15 +301,14 @@ export const useAgentSessionsStore = create<AgentSessionsState>()(

getActiveSessionId: (repoPath, cwd) => {
const state = get();
const activeId = state.activeIds[normalizePath(cwd)];
const key = makeActiveKey(repoPath, cwd);
const activeId = state.activeIds[key];
if (activeId) {
// Verify the session exists and matches repoPath
const session = state.sessions.find((s) => s.id === activeId);
if (session && session.repoPath === repoPath) {
if (session) {
return activeId;
}
}
// Fallback to first session for this repo+cwd
const firstSession = state.sessions.find(
(s) => s.repoPath === repoPath && pathsEqual(s.cwd, cwd)
);
Expand Down Expand Up @@ -530,19 +541,37 @@ export const useAgentSessionsStore = create<AgentSessionsState>()(
);

/**
* Selector hook: get active session ID for a given worktree path.
* Falls back to the first session under that cwd.
* Selector hook: get active session ID for a given repo+worktree pair.
* Falls back to the first session under that repo+cwd.
*
* @param cwd - The worktree path (required).
* @param repoPath - Optional repo path for cross-repo isolation. If omitted, falls back to matching by cwd only.
*/
export function useActiveSessionId(cwd: string | undefined | null): string | null {
export function useActiveSessionId(
cwd: string | undefined | null,
repoPath?: string | undefined | null
): string | null {
return useAgentSessionsStore((state) => {
if (!cwd) return null;
if (repoPath) {
const key = makeActiveKey(repoPath, cwd);
const activeId = state.activeIds[key];
if (activeId) {
const session = state.sessions.find((s) => s.id === activeId);
if (session) return activeId;
}
const first = state.sessions.find(
(s) => s.repoPath === repoPath && normalizePath(s.cwd) === normalizePath(cwd)
);
return first?.id ?? null;
}
// Backward compatibility: match by cwd only
const key = normalizePath(cwd);
const activeId = state.activeIds[key];
if (activeId) {
const session = state.sessions.find((s) => s.id === activeId);
if (session) return activeId;
}
// Fallback to first session for this cwd
const first = state.sessions.find((s) => normalizePath(s.cwd) === key);
return first?.id ?? null;
});
Expand Down
Loading