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:
@@ -27,3 +27,10 @@ export const normalizePath = (value?: string | null): string | null => {
|
||||
const stripped = replaced.length > 1 ? replaced.replace(/\/+$/, "") : replaced;
|
||||
return stripped || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The directory key every terminal surface must agree on: the terminal store's
|
||||
* map keys, the server `cwd` values grouped from a session listing, and the
|
||||
* sidebar indicators. Empty when the value is not a usable path.
|
||||
*/
|
||||
export const normalizeTerminalDirectory = (value: string): string => normalizePath(value) ?? '';
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import type { TerminalAPI, TerminalHandlers } from './api/types';
|
||||
import type { TerminalAPI, TerminalHandlers, TerminalServerSession } from './api/types';
|
||||
import { normalizeTerminalDirectory } from './pathNormalization';
|
||||
import {
|
||||
createProjectActionTerminalSession,
|
||||
groupTerminalSessionsByDirectory,
|
||||
normalizeProjectActionCommand,
|
||||
reconcileTerminalSessionAuthority,
|
||||
stopProjectActionTerminalSession,
|
||||
@@ -301,3 +303,22 @@ test('cancelling a local request does not close a run adopted from another clien
|
||||
})).rejects.toThrow('PROJECT_ACTION_RUN_CANCELLED');
|
||||
expect(closed).toEqual([]);
|
||||
});
|
||||
|
||||
test('a whole-server listing groups under the same directory keys the terminal store uses', () => {
|
||||
// The sidebar reconciles by these keys while the terminal panel reconciles by
|
||||
// the store's own key for the same folder; a Windows path must not split into
|
||||
// two namespaces (`c:\\repo` from the server, `C:/repo` from the sidebar).
|
||||
const session = (sessionId: string, cwd: string): TerminalServerSession => ({
|
||||
sessionId, cwd, status: 'running', createdAt: 1, mode: 'command',
|
||||
purpose: { type: 'project-action', actionId: 'dev', executionId: sessionId },
|
||||
});
|
||||
const grouped = groupTerminalSessionsByDirectory([
|
||||
session('a', 'c:\\repo'),
|
||||
session('b', 'C:/repo/'),
|
||||
session('c', '/srv/app/'),
|
||||
]);
|
||||
|
||||
expect([...grouped.keys()].sort()).toEqual(['/srv/app', 'C:/repo']);
|
||||
expect(grouped.get('C:/repo')?.map(entry => entry.sessionId)).toEqual(['a', 'b']);
|
||||
expect(normalizeTerminalDirectory('c:\\repo')).toBe('C:/repo');
|
||||
});
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import { normalizeProjectActionDirectory } from './projectActions';
|
||||
import { normalizeTerminalDirectory as normalizeDirectory } from './pathNormalization';
|
||||
import { getRuntimeKey } from './runtime-switch';
|
||||
import type { CreateTerminalOptions, TerminalAPI, TerminalServerSession, TerminalSession, TerminalSessionPurpose } from './api/types';
|
||||
|
||||
type TerminalActionMutationRevisions = ReadonlyMap<string, number>;
|
||||
|
||||
const normalizeDirectory = (dir: string): string => {
|
||||
let normalized = dir.trim();
|
||||
while (normalized.length > 1 && normalized.endsWith('/')) {
|
||||
normalized = normalized.slice(0, -1);
|
||||
}
|
||||
return normalized;
|
||||
};
|
||||
|
||||
type ProjectActionTerminalCreateOptions = Omit<Extract<CreateTerminalOptions, { mode: 'command' }>, 'mode' | 'command' | 'sessionId'>;
|
||||
|
||||
type CreateProjectActionTerminalSessionOptions = {
|
||||
@@ -256,10 +248,11 @@ export const reconcileTerminalSessionAuthority = (
|
||||
};
|
||||
|
||||
|
||||
/** Groups a whole-server listing into the directory keys the terminal store uses. */
|
||||
export const groupTerminalSessionsByDirectory = (sessions: TerminalServerSession[]): Map<string, TerminalServerSession[]> => {
|
||||
const groups = new Map<string, TerminalServerSession[]>();
|
||||
for (const session of sessions) {
|
||||
const directory = normalizeProjectActionDirectory(session.cwd);
|
||||
const directory = normalizeDirectory(session.cwd);
|
||||
const group = groups.get(directory);
|
||||
if (group) group.push(session);
|
||||
else groups.set(directory, [session]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TerminalAPI } from './api/types';
|
||||
import { normalizeProjectActionDirectory } from './projectActions';
|
||||
import { normalizeTerminalDirectory } from './pathNormalization';
|
||||
import { groupTerminalSessionsByDirectory, reconcileTerminalSessionAuthority } from './projectActionTerminal';
|
||||
import { getRuntimeKey, subscribeRuntimeEndpointChanged } from './runtime-switch';
|
||||
|
||||
@@ -19,7 +19,7 @@ export const observeTerminalSessions = (
|
||||
listener: Listener,
|
||||
): (() => void) => {
|
||||
if (!terminal.listSessions) return () => {};
|
||||
const key = normalizeProjectActionDirectory(directory);
|
||||
const key = normalizeTerminalDirectory(directory);
|
||||
let observation = observations.get(terminal);
|
||||
if (!observation) {
|
||||
const scopes = new Map<string, Scope>();
|
||||
|
||||
Reference in New Issue
Block a user