fix(runtime): stop git-classifying folder workspaces during session hydration (root cause of #8283)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-07-13 01:33:00 -07:00
co-authored by Orca
parent 084d48bdeb
commit 4a8328db6b
2 changed files with 65 additions and 6 deletions
@@ -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:<pgid>::<folderPath>::workspace:<uuid>`. Since it
// carries no `worktree:`/`folder:` prefix, the placeholder builder's folder
// guard misses it, and splitWorktreeId (first `::`) leaves the
// `::workspace:<uuid>` 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:<uuid>` 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'
+23 -6
View File
@@ -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:<pgid>::<folderPath>::workspace:<uuid>` and carry no
// `worktree:`/`folder:` prefix, so the folder guard above misses them and
// splitWorktreeId (first `::`) leaves the `::workspace:<uuid>` 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,