From a9b9dfbe99b7b809a5e2435a6270fafa17a3173e Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sat, 1 Aug 2026 05:29:50 -0700 Subject: [PATCH] fix(pty): preserve byte-exact reservation paths --- src/shared/stable-pane-id.test.ts | 58 ++++++++++++++++++++++++------- src/shared/stable-pane-id.ts | 22 ++++++++---- 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/src/shared/stable-pane-id.test.ts b/src/shared/stable-pane-id.test.ts index 5d8557feff7..0336ef696a4 100644 --- a/src/shared/stable-pane-id.test.ts +++ b/src/shared/stable-pane-id.test.ts @@ -9,6 +9,20 @@ import { } from './stable-pane-id' const LEAF_ID = '11111111-1111-4111-8111-111111111111' +const PANE_KEY = makePaneKey('tab-1', LEAF_ID) + +function reservationKey( + workspaceId: string, + scope: { connectionId?: string; executionRuntime?: string } = {} +) { + return makePaneSpawnReservationKey({ + paneKey: PANE_KEY, + providerId: 'provider-1', + workspaceId, + sessionId: 'session-1', + ...scope + }) +} describe('stable pane ids', () => { it('recognizes UUID leaf ids as stable pane ids', () => { @@ -57,30 +71,50 @@ describe('stable pane ids', () => { expect(parseLegacyNumericPaneKey('tab:1:12')).toBeNull() }) - it('canonicalizes worktree aliases without collapsing independent PTY scopes', () => { - const paneKey = makePaneKey('tab-1', LEAF_ID) + it.each([ + ['local', { executionRuntime: 'native' }], + ['SSH', { connectionId: 'ssh-1' }] + ])('keeps NFC and NFD POSIX workspace paths distinct on %s', (_label, scope) => { + expect(reservationKey('repo-1::/workspaces/café', scope)).not.toBe( + reservationKey('repo-1::/workspaces/cafe\u0301', scope) + ) + }) + + it.each([ + ['drive', 'repo-1::C:\\Work\\Project\\', 'repo-1::c:/work/project'], + ['UNC', 'repo-1::\\\\Server\\Share\\Project\\', 'repo-1:://server/share/project'], + [ + 'WSL UNC', + 'repo-1::\\\\wsl.localhost\\Ubuntu\\home\\Project', + 'repo-1:://wsl$/ubuntu/home/Project' + ] + ])('canonicalizes equivalent Windows %s paths', (_label, left, right) => { + expect(reservationKey(left)).toBe(reservationKey(right)) + }) + + it('canonicalizes raw and worktree-scoped aliases', () => { + expect(reservationKey('repo-1::/workspaces/project')).toBe( + reservationKey('worktree:repo-1::/workspaces/project') + ) + }) + + it('does not collapse independent PTY scopes', () => { const base = { - paneKey, + paneKey: PANE_KEY, providerId: 'provider-1', connectionId: 'ssh-1', executionRuntime: 'wsl:Ubuntu', - workspaceId: 'worktree:repo-1::C:\\Work\\Project\\', + workspaceId: 'worktree:repo-1::/workspaces/project', sessionId: 'session-1' } const canonical = makePaneSpawnReservationKey(base) - expect(canonical).toBe( - makePaneSpawnReservationKey({ - ...base, - workspaceId: 'repo-1::c:/work/project' - }) - ) for (const distinct of [ { providerId: 'provider-2' }, { connectionId: 'ssh-2' }, { executionRuntime: 'native' }, - { workspaceId: 'repo-1::C:\\Work\\Other' }, - { workspaceId: 'folder:repo-1::C:\\Work\\Project' }, + { workspaceId: 'repo-1::/workspaces/other' }, + { workspaceId: 'folder:repo-1::/workspaces/project' }, { sessionId: 'session-2' } ]) { expect(makePaneSpawnReservationKey({ ...base, ...distinct })).not.toBe(canonical) diff --git a/src/shared/stable-pane-id.ts b/src/shared/stable-pane-id.ts index 3d1480c2c2d..858d6c95ffb 100644 --- a/src/shared/stable-pane-id.ts +++ b/src/shared/stable-pane-id.ts @@ -1,4 +1,4 @@ -import { normalizeRuntimePathForComparison } from './cross-platform-path' +import { isWindowsAbsolutePathLike, normalizeRuntimePathSeparators } from './cross-platform-path' import { parseWorkspaceKey } from './workspace-scope' import { splitWorktreeId } from './worktree-id' @@ -37,22 +37,32 @@ export function makePaneKey(tabId: string, stableLeafId: string): PaneKey { return `${tabId}:${stableLeafId}` as PaneKey } +function canonicalPaneReservationWorkspacePath(worktreePath: string): string { + // Why: POSIX and SSH paths can be byte-sensitive; fold only syntactically proven Windows paths. + if (!isWindowsAbsolutePathLike(worktreePath)) { + return worktreePath + } + const normalized = normalizeRuntimePathSeparators(worktreePath) + const trimmed = /^[A-Za-z]:\/$/.test(normalized) ? normalized : normalized.replace(/\/+$/, '') + const wslUnc = trimmed.match(/^\/\/(?:wsl\.localhost|wsl\$)\/([^/]+)(\/[\s\S]*)?$/i) + return wslUnc ? `//wsl/${wslUnc[1].toLowerCase()}${wslUnc[2] ?? ''}` : trimmed.toLowerCase() +} + function canonicalWorkspaceIdentity(workspaceId: string): readonly string[] | null { - const trimmed = workspaceId.trim() - if (!trimmed || trimmed.length > 1024) { + if (!workspaceId.trim() || workspaceId.length > 1024) { return null } - const scope = parseWorkspaceKey(trimmed) + const scope = parseWorkspaceKey(workspaceId) const kind = scope?.type ?? 'worktree' const unscopedId = scope?.type === 'worktree' ? scope.worktreeId : scope?.type === 'folder' ? scope.folderWorkspaceId - : trimmed + : workspaceId const parsed = splitWorktreeId(unscopedId) return parsed - ? [kind, parsed.repoId, normalizeRuntimePathForComparison(parsed.worktreePath)] + ? [kind, parsed.repoId, canonicalPaneReservationWorkspacePath(parsed.worktreePath)] : [kind, unscopedId] }