From 4a8328db6bfec1ef6aabb32b414d3be9b764ad17 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 13 Jul 2026 01:33:00 -0700 Subject: [PATCH] fix(runtime): stop git-classifying folder workspaces during session hydration (root cause of #8283) Co-authored-by: Orca --- .../store/slices/terminals-hydration.test.ts | 42 +++++++++++++++++++ src/renderer/src/store/slices/terminals.ts | 29 ++++++++++--- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/store/slices/terminals-hydration.test.ts b/src/renderer/src/store/slices/terminals-hydration.test.ts index 60502611a66..6ee4962cd4f 100644 --- a/src/renderer/src/store/slices/terminals-hydration.test.ts +++ b/src/renderer/src/store/slices/terminals-hydration.test.ts @@ -97,6 +97,8 @@ import { getDefaultWorkspaceSession } from '../../../../shared/constants' import { folderWorkspaceKey, worktreeWorkspaceKey } from '../../../../shared/workspace-scope' +import { isGitRepoKind } from '../../../../shared/repo-kind' +import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR } from '../../../../shared/worktree-id' import { createTestStore, makeLayout, @@ -288,6 +290,46 @@ describe('hydrateWorkspaceSession', () => { }) }) + it('does not synthesize a git placeholder from a runtime folder-workspace instance id (#8283)', () => { + const store = createTestStore() + // A folder (non-git) project's workspace-instance id keeps the runtime + // shape `folder-workspace:::::workspace:`. Since it + // carries no `worktree:`/`folder:` prefix, the placeholder builder's folder + // guard misses it, and splitWorktreeId (first `::`) leaves the + // `::workspace:` suffix on the derived path. + const folderRepoId = 'folder-workspace:pg1' + const folderPath = '/home/user' + const worktreeId = `${folderRepoId}::${folderPath}${FOLDER_WORKSPACE_INSTANCE_SEPARATOR}11111111-1111-1111-1111-111111111111` + const session: WorkspaceSessionState = { + ...getDefaultWorkspaceSession(), + activeRepoId: folderRepoId, + activeWorktreeId: worktreeId, + activeTabId: 'folder-instance-tab', + tabsByWorktree: { + [worktreeId]: [ + makeTab({ id: 'folder-instance-tab', worktreeId, ptyId: 'folder-instance-session' }) + ] + } + } + + store.getState().hydrateWorkspaceSession(session, { + runtimeHostIdByWorkspaceSessionKey: { [worktreeId]: 'runtime:env-1' } + }) + + // The synthetic `::workspace:` suffix is not a filesystem path; if it + // reaches a worktree path it becomes a Git cwd and fails with a continuous + // `spawn git ENOENT` on every status poll (#8283). + for (const worktree of store.getState().worktreesByRepo[folderRepoId] ?? []) { + expect(worktree.path).not.toContain(FOLDER_WORKSPACE_INSTANCE_SEPARATOR) + } + // The placeholder repo must be classified as a folder repo so the + // isGitRepoKind-gated status poll never spawns Git for it. + const placeholderRepo = store.getState().repos.find((repo) => repo.id === folderRepoId) + if (placeholderRepo) { + expect(isGitRepoKind(placeholderRepo)).toBe(false) + } + }) + it('moves restored active focus from a dead split leaf to a pty-backed sibling', () => { const store = createTestStore() const worktreeId = 'repo1::/wt-1' diff --git a/src/renderer/src/store/slices/terminals.ts b/src/renderer/src/store/slices/terminals.ts index 817597d4132..0c958e0609b 100644 --- a/src/renderer/src/store/slices/terminals.ts +++ b/src/renderer/src/store/slices/terminals.ts @@ -34,7 +34,11 @@ import { parsePaneKey } from '../../../../shared/stable-pane-id' import { isValidHostTerminalTabId, isValidTerminalTabId } from '../../../../shared/terminal-tab-id' -import { getRepoIdFromWorktreeId, splitWorktreeId } from '../../../../shared/worktree-id' +import { + getRepoIdFromWorktreeId, + splitWorktreeId, + splitWorktreeIdForFilesystem +} from '../../../../shared/worktree-id' import { isWslUncPath } from '../../../../shared/wsl-paths' import type { ProjectExecutionRuntimeResolution } from '../../../../shared/project-execution-runtime' import type { StartupCommandDelivery } from '../../../../shared/codex-startup-delivery' @@ -172,6 +176,18 @@ function buildRuntimeSessionPlaceholders({ if (!parsed) { continue } + // Why (#8283): folder (non-git) workspace instances keep the runtime shape + // `folder-workspace:::::workspace:` and carry no + // `worktree:`/`folder:` prefix, so the folder guard above misses them and + // splitWorktreeId (first `::`) leaves the `::workspace:` suffix on the + // path. That synthetic, nonexistent path reaches Git as a cwd and fails with + // a continuous `spawn git ENOENT` on every status poll. Resolve the real + // folder path and tag the placeholder as a folder repo so the + // isGitRepoKind-gated status poll never spawns Git for it. The suffix-strip + // itself (not a substring match) is the folder-instance signal. + const worktreePath = + splitWorktreeIdForFilesystem(worktreeId)?.worktreePath ?? parsed.worktreePath + const isFolderWorkspace = worktreePath !== parsed.worktreePath const existingRepo = nextRepos.some((repo) => repo.id === parsed.repoId) if (!existingRepo) { // Why: remote catalogs load after hydration, but host-split session @@ -181,12 +197,13 @@ function buildRuntimeSessionPlaceholders({ ...nextRepos, { id: parsed.repoId, - path: parsed.worktreePath, - displayName: getPathDisplayName(parsed.worktreePath, parsed.repoId), + path: worktreePath, + displayName: getPathDisplayName(worktreePath, parsed.repoId), badgeColor: DEFAULT_REPO_BADGE_COLOR, addedAt: 0, connectionId: null, - executionHostId: hostId + executionHostId: hostId, + ...(isFolderWorkspace ? { kind: 'folder' as const } : {}) } ] } @@ -198,7 +215,7 @@ function buildRuntimeSessionPlaceholders({ id: worktreeId, repoId: parsed.repoId, hostId, - displayName: getPathDisplayName(parsed.worktreePath, parsed.repoId), + displayName: getPathDisplayName(worktreePath, parsed.repoId), comment: '', linkedIssue: null, linkedPR: null, @@ -210,7 +227,7 @@ function buildRuntimeSessionPlaceholders({ isPinned: false, sortOrder: 0, lastActivityAt: 0, - path: parsed.worktreePath, + path: worktreePath, head: '', branch: '', isBare: false,