fix(terminal): key project action state by one canonical directory

The terminal store keyed its directories by trimming trailing slashes,
while the new sidebar activity indicator and the whole-server session
grouping keyed the same folder with the project-action normalizer, which
also rewrites backslashes. On Windows that split one project into two
namespaces: the sidebar reconciled server sessions under "C:/repo" while
the terminal panel and the project actions button worked under "C:\repo",
so the running-action indicator never lit up, adopted tabs were duplicated
and a stop recorded in one namespace did not guard reconciliation in the
other. The project actions button also read the store map directly with a
path normalized somewhere else, missing its own directory entry.

All terminal directory keys now come from normalizeTerminalDirectory in
lib/pathNormalization, and store reads go through getDirectoryState.
This commit is contained in:
Bohdan Triapitsyn
2026-09-05 14:48:25 +03:00
parent 74b8c77d49
commit 03e14b61cd
10 changed files with 62 additions and 26 deletions
@@ -226,13 +226,16 @@ export const ProjectActionsButton = ({
contextHostDirectoryRef.current = contextHostDirectory;
}, [contextHostDirectory]);
// The store owns its directory key form; reading `sessions` directly with a
// project-action-normalized path misses the entry whenever the two spellings
// differ (Windows drive letters and separators).
const directoryTerminalState = useTerminalStore((state) => (
normalizedDirectory ? state.sessions.get(normalizedDirectory) : undefined
normalizedDirectory ? state.getDirectoryState(normalizedDirectory) : undefined
));
const projectTerminalState = useTerminalStore((state) => (
normalizedProjectDirectory && normalizedProjectDirectory !== normalizedDirectory
? state.sessions.get(normalizedProjectDirectory)
? state.getDirectoryState(normalizedProjectDirectory)
: undefined
));
@@ -1065,7 +1068,7 @@ export const ProjectActionsButton = ({
const previewRun = previewAction ? projectActionRuns[toProjectActionRunKey(executionDirectoryFor(previewAction), previewAction.id)] : null;
const selectedRunPreviewUrl = useTerminalStore((state) => {
if (!previewRun) return null;
return state.sessions.get(previewRun.directory)?.tabs.find((tab) => tab.id === previewRun.tabId)?.previewUrl ?? null;
return state.getDirectoryState(previewRun.directory)?.tabs.find((tab) => tab.id === previewRun.tabId)?.previewUrl ?? null;
});
if (runtime.isVSCode || (!allowMobile && isMobile) || !stableProjectRef || !normalizedDirectory) {
@@ -1,14 +1,14 @@
import React from 'react';
import { Icon } from '@/components/icon/Icon';
import { useI18n } from '@/lib/i18n';
import { normalizeProjectActionDirectory } from '@/lib/projectActions';
import { normalizeTerminalDirectory } from '@/lib/pathNormalization';
import { ACTIVE_PROJECT_ACTION_LIFECYCLES, useTerminalStore } from '@/stores/useTerminalStore';
import { cn } from '@/lib/utils';
/** A directory-scoped leaf subscription; output chunks do not rerender the indicator. */
export const DirectoryActionIndicator = ({ directory, className }: { directory: string; className?: string }) => {
const { t } = useI18n();
const key = normalizeProjectActionDirectory(directory);
const key = normalizeTerminalDirectory(directory);
const state = useTerminalStore(React.useCallback(store => store.sessions.get(key), [key]));
const active = state?.tabs.some(tab => tab.purpose.type === 'project-action'
&& tab.purpose.executionId !== null && ACTIVE_PROJECT_ACTION_LIFECYCLES.has(tab.lifecycle));
@@ -62,7 +62,7 @@ export const TerminalView: React.FC<TerminalViewProps> = ({ visible, directory }
const targetDirectory = directory ?? null;
const terminalDirectory = targetDirectory || contextDirectory;
const hasExplicitTerminalTarget = targetDirectory !== null;
const directoryTerminalState = useTerminalStore((s) => terminalDirectory ? s.sessions.get(terminalDirectory) : undefined);
const directoryTerminalState = useTerminalStore((s) => terminalDirectory ? s.getDirectoryState(terminalDirectory) : undefined);
const terminalHydrated = useTerminalStore((s) => s.hasHydrated);
const ensureDirectory = useTerminalStore((s) => s.ensureDirectory);
const createTab = useTerminalStore((s) => s.createTab);