Merge remote-tracking branch 'origin/main' into brennanb2025/global-worktree-visibility-defaults

# Conflicts:
#	src/main/ipc/worktrees.ts
#	src/main/persistence.ts
#	src/preload/api-types.ts
#	src/renderer/src/components/sidebar/WorktreeVisibilityDialog.tsx
#	src/shared/types.ts
#	src/shared/worktree/ownership.test.ts
#	src/shared/worktree/ownership.ts
#	src/shared/worktree/visibility-sources.test.ts
This commit is contained in:
Brennan Benson
2026-08-13 22:30:23 -07:00
440 changed files with 10679 additions and 8595 deletions
+1 -3
View File
@@ -167,7 +167,6 @@ inline src/main/window/attach-main-window-services.ts
inline src/main/window/createMainWindow.test.ts
inline src/main/window/createMainWindow.ts
inline src/main/workspace-space-analysis.ts
inline src/preload/api-types.ts
inline src/preload/index.ts
inline src/relay/agent-hook-server.ts
inline src/relay/dispatcher.ts
@@ -318,7 +317,7 @@ inline src/shared/agent-hook-listener.ts
inline src/shared/automation-schedules.ts
inline src/shared/commit-message-agent-spec.ts
inline src/shared/constants.ts
inline src/shared/github-project-types.ts
inline src/shared/github/project-types.ts
inline src/shared/keybindings.test.ts
inline src/shared/keybindings.ts
inline src/shared/marine-creatures.ts
@@ -327,7 +326,6 @@ inline src/shared/runtime-types.ts
inline src/shared/source-control-ai.ts
inline src/shared/telemetry-events.ts
inline src/shared/text-search.ts
inline src/shared/types.ts
inline tests/e2e/helpers/terminal.ts
inline tests/e2e/terminal-panes.spec.ts
mobile-config app/h/*/files/*.tsx
@@ -1,6 +1,6 @@
#!/usr/bin/env node
import { performance } from 'node:perf_hooks'
import { planWorktreeSortOrderUpdates } from '../../src/shared/worktree-sort-order-update.ts'
import { planWorktreeSortOrderUpdates } from '../../src/shared/worktree/sort-order-update.ts'
import { reuseEqualCatalogRows } from '../../src/renderer/src/store/slices/worktree-catalog-reconciliation.ts'
const WORKTREE_COUNT = 655
+1
View File
@@ -5,6 +5,7 @@
"../src/renderer/src/**/*",
"../src/renderer/src/**/*.tsx",
"../src/preload/api-types.ts",
"../src/preload/api/**/*",
"../src/shared/**/*",
"../src/main/gitlab/mappers.ts",
"../src/main/ipc/worktree-branch-name.ts",
+1
View File
@@ -5,6 +5,7 @@
"../src/renderer/src/**/*",
"../src/renderer/src/**/*.tsx",
"../src/preload/api-types.ts",
"../src/preload/api/**/*",
"../src/shared/**/*"
],
"compilerOptions": {
+3 -3
View File
@@ -119,11 +119,11 @@ import {
isIterationCurrent,
sortRows,
type ProjectGroup
} from '../../../../src/shared/github-project-group-sort'
} from '../../../../src/shared/github/project-group-sort'
import type {
GitHubProjectSortDirection,
GitHubProjectTable as SharedGitHubProjectTable
} from '../../../../src/shared/github-project-types'
} from '../../../../src/shared/github/project-types'
import {
CROSS_REPO_DISPLAY_LIMIT,
isGitHubWorkItemsSshRemoteRequiredError,
@@ -161,7 +161,7 @@ import type { HostedReviewDecision } from '../../../../src/shared/hosted-review'
import {
githubProjectHost,
githubProjectIdentityKey as githubProjectKey
} from '../../../../src/shared/github-project-identity'
} from '../../../../src/shared/github/project-identity'
type RepoSummary = {
id: string
@@ -1,6 +1,6 @@
import { isPathInsideOrEqual, isRuntimePathAbsolute } from '../../../src/shared/cross-platform-path'
import { parseWslUncPath } from '../../../src/shared/wsl-paths'
import { splitWorktreeIdForFilesystem } from '../../../src/shared/worktree-id'
import { splitWorktreeIdForFilesystem } from '../../../src/shared/worktree/id'
import type { AiVaultSession } from '../../../src/shared/ai-vault-types'
import type { Worktree } from '../worktree/workspace-list-types'
+1 -1
View File
@@ -9,7 +9,7 @@ import type { HostedReviewInfo } from '../../../src/shared/hosted-review'
import {
normalizeGitHubPRForBranchOutcome,
type GitHubPRForBranchResponse
} from '../../../src/shared/github-pr-for-branch-outcome'
} from '../../../src/shared/github/pull-request-for-branch-outcome'
import type { RpcClient } from '../transport/rpc-client'
import type { RpcSuccess } from '../transport/types'
import { mobileRepoSelectorFromWorktreeId } from '../source-control/mobile-pr-create'
+1 -1
View File
@@ -1,5 +1,5 @@
import type { GitHubPRMergeMethod, PRState } from '../../../src/shared/types'
import { githubRepoIdentityKey } from '../../../src/shared/github-repository-identity-key'
import { githubRepoIdentityKey } from '../../../src/shared/github/repository-identity-key'
import { classifyPrSidebarFailure } from './mobile-pr-sidebar-state'
import { createOptimisticField, type OptimisticField } from './optimistic-write-sequence'
import type { GitHubPrMutationOutcome } from './github-pr-mutations'
+1 -1
View File
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useFocusEffect } from 'expo-router'
import type { RuntimeClientEventStreamMessage } from '../../../src/shared/runtime-client-events'
import { getRepoIdFromWorktreeId } from '../../../src/shared/worktree-id'
import { getRepoIdFromWorktreeId } from '../../../src/shared/worktree/id'
import type { RpcClient } from '../transport/rpc-client'
import type { ConnectionState, RpcSuccess } from '../transport/types'
import { getLiveWorktreeDisplayName, type WorktreeDisplayNameSource } from './worktree-display-name'
+1 -1
View File
@@ -1,4 +1,4 @@
import type { GitHubProjectIdentity } from '../../../src/shared/github-project-identity'
import type { GitHubProjectIdentity } from '../../../src/shared/github/project-identity'
export type GitHubProjectOwnerType = GitHubProjectIdentity['ownerType']
export type GitHubProjectRef = GitHubProjectIdentity
@@ -1,7 +1,7 @@
import {
githubRepoIdentityKey,
isDefaultGitHubHost
} from '../../../src/shared/github-repository-identity-key'
} from '../../../src/shared/github/repository-identity-key'
export type GitHubProjectRepoMatch = {
id: string
@@ -9,7 +9,7 @@ import { parseGitLabIssueOrMRLink } from '../../../src/shared/new-workspace/gitl
import { isSmartWorkspaceSourceQueryWithinLimit } from '../../../src/shared/new-workspace/smart-workspace-source-results'
import type { RpcClient } from '../transport/rpc-client'
import type { RpcSuccess } from '../transport/types'
import { githubRepoIdentityKey } from '../../../src/shared/github-repository-identity-key'
import { githubRepoIdentityKey } from '../../../src/shared/github/repository-identity-key'
// A repo the picker can switch to for a cross-repo GitHub paste. Slug is derived
// best-effort from the repo's remote metadata.
+1 -1
View File
@@ -1,7 +1,7 @@
import type {
LinearMcpIssueListRequest,
LinearMcpIssueListResult
} from '../../shared/linear-agent-access'
} from '../../shared/linear/agent-access'
import type { CommandHandler } from '../dispatch'
import {
getOptionalNonNegativeIntegerFlag,
+1 -1
View File
@@ -2,7 +2,7 @@ import type {
LinearIssueRelationship,
LinearIssueRelationWriteRequest,
LinearIssueRelationWriteResult
} from '../../shared/linear-agent-access'
} from '../../shared/linear/agent-access'
import type { CommandHandler } from '../dispatch'
import { getRequiredStringFlag } from '../flags'
import { printResult } from '../format'
+1 -1
View File
@@ -1,7 +1,7 @@
import type {
LinearSaveIssueRequest,
LinearSaveIssueResult
} from '../../shared/linear-agent-access'
} from '../../shared/linear/agent-access'
import type { CommandHandler } from '../dispatch'
import { printResult } from '../format'
import { formatLinearSaveIssue } from '../linear-format'
+2 -2
View File
@@ -19,8 +19,8 @@ import type {
LinearTeamListResult,
LinearTeamMembersResult,
LinearTeamStatesResult
} from '../../shared/linear-agent-access'
import { clampLinearSearchLimit } from '../../shared/linear-agent-access'
} from '../../shared/linear/agent-access'
import { clampLinearSearchLimit } from '../../shared/linear/agent-access'
import type { CommandHandler } from '../dispatch'
import { printResult } from '../format'
import { RuntimeClientError } from '../runtime-client'
@@ -2,7 +2,7 @@ import {
buildLinearIssueLinkUpdates,
LINEAR_ISSUE_LINK_CLEARED,
type LinearIssueLinkUpdates
} from '../../shared/linear-links'
} from '../../shared/linear/links'
import { RuntimeClientError } from '../runtime-client'
export function getOptionalLinearIssueLinkFlag(
+1 -1
View File
@@ -5,7 +5,7 @@ import type {
LinearMcpIssueListResult,
LinearProjectListResult,
LinearSearchResult
} from '../shared/linear-agent-access'
} from '../shared/linear/agent-access'
import {
formatLinearCreate,
formatLinearIssue,
+2 -2
View File
@@ -16,11 +16,11 @@ import type {
LinearTeamMembersResult,
LinearTeamStatesResult,
LinearStatusSetResult
} from '../shared/linear-agent-access'
} from '../shared/linear/agent-access'
import {
formatLinearProjectListRows,
linearProjectListWarningLines
} from '../shared/linear-project-list-format'
} from '../shared/linear/project-list-format'
export function formatLinearIssue(result: LinearIssueContextResult): string {
const issue = result.issue
+3 -3
View File
@@ -6,13 +6,13 @@ import type {
LinearIssueRequest,
LinearIssueTaskUpdateRequest,
LinearWriteTargetRequest
} from '../shared/linear-agent-access'
} from '../shared/linear/agent-access'
import {
LINEAR_CHILDREN_MAX_DEPTH,
LINEAR_WRITE_BODY_CAP,
clampLinearIssueDepth
} from '../shared/linear-agent-access'
import { isLinearUuid } from '../shared/linear-uuid'
} from '../shared/linear/agent-access'
import { isLinearUuid } from '../shared/linear/uuid'
import {
getOptionalNonNegativeIntegerFlag,
getOptionalStringFlag,
+1 -1
View File
@@ -1,4 +1,4 @@
import type { LinearSaveIssueRequest } from '../shared/linear-agent-access'
import type { LinearSaveIssueRequest } from '../shared/linear/agent-access'
import {
getOptionalNullableNumberFlag,
getOptionalStringFlag,
@@ -1,6 +1,6 @@
import type { vi } from 'vitest'
import type { GlobalSettings, Repo } from '../../shared/types'
import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree-id'
import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree/id'
import type {
FirstWorkBranchRenameDeps,
FirstWorkBranchRenameEvent
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { GlobalSettings, Repo } from '../../shared/types'
import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree-id'
import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree/id'
const {
gitExecFileAsyncMock,
@@ -1,6 +1,6 @@
// On first agent work in a fresh workspace, replace the auto-generated creature branch (e.g. `you/Nautilus`) with a short work-derived name.
import type { GlobalSettings, Repo } from '../../shared/types'
import { getRepoIdFromWorktreeId, splitWorktreeIdForFilesystem } from '../../shared/worktree-id'
import { getRepoIdFromWorktreeId, splitWorktreeIdForFilesystem } from '../../shared/worktree/id'
import { parseWorkspaceKey } from '../../shared/workspace-scope'
import { parsePaneKey } from '../../shared/stable-pane-id'
import {
@@ -10,7 +10,7 @@ import {
FOLDER_WORKSPACE_INSTANCE_SEPARATOR,
getRepoIdFromWorktreeId,
splitWorktreeIdForFilesystem
} from '../../shared/worktree-id'
} from '../../shared/worktree/id'
import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
import { planWorktreeFolderRename } from '../ipc/worktree-folder-rename-target'
@@ -5,7 +5,7 @@ import {
type ExecutionHostId
} from '../../shared/execution-host'
import type { FolderWorkspace, ProjectGroup } from '../../shared/types'
import { getRepoIdFromWorktreeId } from '../../shared/worktree-id'
import { getRepoIdFromWorktreeId } from '../../shared/worktree/id'
import { parseWorkspaceKey } from '../../shared/workspace-scope'
export type RestoredSubagentLivenessSweepDeps = {
@@ -3,7 +3,7 @@ import type { Automation } from '../../shared/automations-types'
import { getAutomationLegacyRepoId } from '../../shared/automation-run-identity'
import { getRepoExecutionHostId, parseExecutionHostId } from '../../shared/execution-host'
import type { ProjectHostSetup, Repo } from '../../shared/types'
import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id'
import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id'
export type AutomationRunTargetResult =
| { ok: true; cwd: string; repo: Repo; setup?: ProjectHostSetup }
+1 -1
View File
@@ -1,4 +1,4 @@
import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id'
import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id'
import { isWslShellName } from '../../shared/local-windows-terminal-runtime'
import { getDefaultWslDistro, parseWslPath } from '../wsl'
import { parsePtySessionId } from './pty-session-id'
+1 -1
View File
@@ -46,7 +46,7 @@ import {
removeSafeUntrackedDiscardTargets
} from '../../shared/git-discard-path-safety'
import { readBranchCompareHead } from '../../shared/git-branch-compare-head'
import { resolveWorktreeAddBaseRef } from '../../shared/worktree-base-ref'
import { resolveWorktreeAddBaseRef } from '../../shared/worktree/base-ref'
import { resolveWorktreeBaseCommitOid } from './worktree-base-ref-probe'
import { getLargeDiffRenderLimit } from '../../shared/large-diff-render-limit'
import { InFlightPromiseDedupe, stableInFlightKey } from '../../shared/in-flight-promise-dedupe'
+3 -3
View File
@@ -5,7 +5,7 @@ import {
branchHasNoUnmergedChangesWithLazyTargetRefresh,
getBranchCleanupTargetRefs
} from '../../shared/git-branch-cleanup'
import { resolveWorktreeAddBaseRef } from '../../shared/worktree-base-ref'
import { resolveWorktreeAddBaseRef } from '../../shared/worktree/base-ref'
import { withSpan } from '../observability/tracer'
import { withWorktreeRemoveStageSpan } from '../observability/instrumentation'
import {
@@ -20,8 +20,8 @@ import type {
LocalBaseRefUpdateSuggestion,
RemoveWorktreeResult
} from '../../shared/types'
import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree-removal'
import { isSubmoduleWorktreeRemovalRefusal } from '../../shared/worktree-submodule-removal'
import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree/removal'
import { isSubmoduleWorktreeRemovalRefusal } from '../../shared/worktree/submodule-removal'
import { decodeGitCQuotedPath } from '../../shared/git-cquoted-path'
import { parseGitRevListAheadBehindCounts } from '../../shared/git-rev-list-output'
import { parseWslUncPath } from '../../shared/wsl-paths'
+1 -1
View File
@@ -12,7 +12,7 @@
* uses stable field labels ("Token scopes:", "(GITHUB_TOKEN)", etc.).
*/
import { ghExecFileAsync } from '../git/runner'
import type { GhAuthDiagnostic, GhAuthAccount } from '../../shared/github-auth-types'
import type { GhAuthDiagnostic, GhAuthAccount } from '../../shared/github/auth-types'
// Required scopes for ProjectV2 GraphQL access in Orca. `project` is the
// scope that gates ProjectV2 reads/writes; the others are needed for the
@@ -118,7 +118,7 @@ import {
_resetMergeQueueCacheForTests,
_resetOwnerRepoCache
} from './client'
import { GITHUB_WORK_ITEMS_QUERY_MAX_BYTES } from '../../shared/github-work-items-query-bounds'
import { GITHUB_WORK_ITEMS_QUERY_MAX_BYTES } from '../../shared/github/work-items-query-bounds'
import { _resetOriginGitHubApiRepositoryCache } from './github-api-repository'
+1 -1
View File
@@ -112,7 +112,7 @@ import {
_resetMergeQueueCacheForTests,
_resetOwnerRepoCache
} from './client'
import { GITHUB_WORK_ITEMS_QUERY_MAX_BYTES } from '../../shared/github-work-items-query-bounds'
import { GITHUB_WORK_ITEMS_QUERY_MAX_BYTES } from '../../shared/github/work-items-query-bounds'
import { _resetOriginGitHubApiRepositoryCache } from './github-api-repository'
+5 -5
View File
@@ -28,10 +28,10 @@ import {
normalizeHostedReviewBaseRef,
normalizeHostedReviewHeadRef
} from '../../shared/hosted-review-refs'
import { normalizeGitHubPRMergeMethodSettings } from '../../shared/github-pr-merge-methods'
import { normalizeGitHubPRMergeMethodSettings } from '../../shared/github/pull-request-merge-methods'
import { summarizeProviderChecks } from '../../shared/provider-check-summary'
import { isGitHubWorkItemsQueryTooLarge } from '../../shared/github-work-items-query-bounds'
import { classifyGitHubUnavailable } from '../../shared/github-api-availability'
import { isGitHubWorkItemsQueryTooLarge } from '../../shared/github/work-items-query-bounds'
import { classifyGitHubUnavailable } from '../../shared/github/api-availability'
import { parseTaskQuery, type ParsedTaskQuery } from '../../shared/task-query'
import {
GITHUB_WORK_ITEMS_SSH_REMOTE_REQUIRED_MESSAGE,
@@ -91,7 +91,7 @@ import {
type GitHubRepoExecOptions,
type GitHubApiRepository
} from './github-api-repository'
import { githubRepoIdentityKey } from '../../shared/github-repository-identity-key'
import { githubRepoIdentityKey } from '../../shared/github/repository-identity-key'
export { _resetOwnerRepoCache } from './gh-utils'
export {
getIssue,
@@ -127,7 +127,7 @@ import {
import {
GITHUB_CHECK_DETAILS_HOST_TIMEOUT_MS,
GITHUB_CHECK_DETAILS_TIMEOUT_MESSAGE
} from '../../shared/github-check-details-deadline'
} from '../../shared/github/check-details-deadline'
import { hydrateGitHubPRStack, mergeGitHubPRStack } from './github-pr-stack'
type GhExecOptions = GitHubRepoExecOptions & { signal?: AbortSignal }
+1 -1
View File
@@ -38,7 +38,7 @@ import {
__resetLocalGitConfigSignatureCacheForTests,
readLocalGitConfigSignature
} from './local-git-config-signature'
import { GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN } from '../../shared/github-work-items-query-bounds'
import { GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN } from '../../shared/github/work-items-query-bounds'
describe('github owner/repo resolution', () => {
beforeEach(() => {
+1 -1
View File
@@ -2,7 +2,7 @@ import type { GitHubOwnerRepo, IssueSourcePreference } from '../../shared/types'
import {
githubRepoIdentityKey,
isDefaultGitHubHost
} from '../../shared/github-repository-identity-key'
} from '../../shared/github/repository-identity-key'
import {
getOwnerRepoForRemote,
ghRepoExecOptions,
@@ -1,5 +1,5 @@
import type { IssueSourcePreference } from '../../shared/types'
import { githubRepoIdentityKey } from '../../shared/github-repository-identity-key'
import { githubRepoIdentityKey } from '../../shared/github/repository-identity-key'
import {
getOwnerRepoForRemote,
type LocalGitExecOptions,
+1 -1
View File
@@ -6,7 +6,7 @@ import type {
PRReviewDecision,
PRState
} from '../../shared/types'
import { githubRepoIdentityKey } from '../../shared/github-repository-identity-key'
import { githubRepoIdentityKey } from '../../shared/github/repository-identity-key'
import { ghExecFileAsync } from '../git/runner'
import {
githubHostExecOptions,
@@ -1,13 +1,8 @@
import { normalizeGitHubRemoteHost } from '../../shared/git-remote-host-alias'
import type { GitHubOwnerRepo } from '../../shared/types'
export type GitHubRemoteIdentity = GitHubOwnerRepo & { host: string }
export function normalizeGitHubRemoteHost(host: string): string {
const normalizedHost = host.toLowerCase()
// Why: GitHub documents ssh.github.com as SSH-over-HTTPS for github.com repos.
return normalizedHost === 'ssh.github.com' ? 'github.com' : normalizedHost
}
// Why: HTTP ports identify the GHES web/API endpoint; SSH and git ports are
// transport-only and must not leak into gh's host identity.
function hostFromRemoteUrl(url: URL): string {
@@ -1,7 +1,7 @@
import { ghExecFileAsync } from './gh-utils'
import { noteRepositoryRateLimitSpend, repositoryRateLimitGuard } from './rate-limit'
import { githubHostExecOptions } from './github-api-repository'
import { githubRepoIdentityKey } from '../../shared/github-repository-identity-key'
import { githubRepoIdentityKey } from '../../shared/github/repository-identity-key'
import type { OwnerRepo } from './github-repository-identity'
type GhExecOptions = Parameters<typeof ghExecFileAsync>[1]
@@ -1,5 +1,5 @@
import type { PRRefreshErrorType } from '../../shared/types'
import { classifyGitHubUnavailable } from '../../shared/github-api-availability'
import { classifyGitHubUnavailable } from '../../shared/github/api-availability'
import { extractExecError } from '../git/exec-error'
/**
+1 -1
View File
@@ -11,7 +11,7 @@ import { beforeEach, describe, expect, it } from 'vitest'
import {
GITHUB_PROJECT_REF_INPUT_MAX_BYTES,
GITHUB_PROJECT_REF_INPUT_TOO_LARGE_ERROR
} from '../../shared/github-project-ref-input'
} from '../../shared/github/project-ref-input'
import {
PROJECT_VIEW_OWNER_CACHE_MAX_ENTRIES,
_getProjectViewCacheSizesForTests,
+3 -3
View File
@@ -46,12 +46,12 @@ import type {
ListProjectViewsResult,
ResolveProjectRefArgs,
ResolveProjectRefResult
} from '../../shared/github-project-types'
} from '../../shared/github/project-types'
import {
GITHUB_PROJECT_REF_INPUT_TOO_LARGE_ERROR,
isGitHubProjectRefInputTooLarge
} from '../../shared/github-project-ref-input'
import { githubProjectHost } from '../../shared/github-project-identity'
} from '../../shared/github/project-ref-input'
import { githubProjectHost } from '../../shared/github/project-identity'
// Re-export the public API so existing `./project-view` call sites keep working; the split is internal-only.
export { isValidOwnerSlug, isValidRepoSlug } from './project-view/internals'
+3 -3
View File
@@ -9,9 +9,9 @@ import {
noteRepositoryRateLimitSpend,
type RateLimitBucketKind
} from '../rate-limit'
import type { GitHubProjectViewError } from '../../../shared/github-project-types'
import { githubProjectHost } from '../../../shared/github-project-identity'
import { isDefaultGitHubHost } from '../../../shared/github-repository-identity-key'
import type { GitHubProjectViewError } from '../../../shared/github/project-types'
import { githubProjectHost } from '../../../shared/github/project-identity'
import { isDefaultGitHubHost } from '../../../shared/github/repository-identity-key'
import { isGitHubHostAuthenticatedForGlobalCli } from '../github-enterprise-repository'
import {
classifyProjectError,
+2 -2
View File
@@ -17,7 +17,7 @@ import {
type GraphqlVars
} from './internals'
import { classifyProjectError, rateLimitedError } from './project-error-classification'
import { githubProjectHost } from '../../../shared/github-project-identity'
import { githubProjectHost } from '../../../shared/github/project-identity'
import type { GitHubAssignableUser, GitHubWorkItemDetails, PRComment } from '../../../shared/types'
import type {
AddIssueCommentBySlugArgs,
@@ -39,7 +39,7 @@ import type {
UpdateIssueTypeBySlugArgs,
UpdatePullRequestBySlugArgs,
UpdateProjectItemFieldArgs
} from '../../../shared/github-project-types'
} from '../../../shared/github/project-types'
function githubHostExecOptions(args: { host?: string }): { host: string } {
return { host: githubProjectHost(args.host) }
@@ -1,8 +1,8 @@
// Why: turns raw gh stderr/stdout into the typed GitHubProjectViewError the
// renderer can act on (auth vs scope vs rate limit vs drift), shared by the
// project-view read and mutation paths.
import type { GitHubProjectViewError } from '../../../shared/github-project-types'
import { githubProjectHost } from '../../../shared/github-project-identity'
import type { GitHubProjectViewError } from '../../../shared/github/project-types'
import { githubProjectHost } from '../../../shared/github/project-identity'
export type GhGraphqlErrorShape = {
type?: string
+1 -1
View File
@@ -9,7 +9,7 @@ import type {
GitHubRateLimitBucket,
GitHubRateLimitSnapshot
} from '../../shared/types'
import { isDefaultGitHubHost } from '../../shared/github-repository-identity-key'
import { isDefaultGitHubHost } from '../../shared/github/repository-identity-key'
import { isWslUncPath } from '../../shared/wsl-paths'
import { acquire, release } from './gh-utils'
import { ghExecFileAsync } from '../git/runner'
+1 -1
View File
@@ -2,7 +2,7 @@ import type {
CreateStackedHostedReviewInput,
CreateStackedHostedReviewResult
} from '../../shared/hosted-review'
import { isDefaultGitHubHost } from '../../shared/github-repository-identity-key'
import { isDefaultGitHubHost } from '../../shared/github/repository-identity-key'
import {
normalizeHostedReviewBaseRef,
normalizeHostedReviewHeadRef
+1 -1
View File
@@ -267,7 +267,7 @@ import {
configureWindowsHostGitEnvironmentReadiness,
setDefaultWslDistroOverride
} from './git/runner'
import { getRepoIdFromWorktreeId } from '../shared/worktree-id'
import { getRepoIdFromWorktreeId } from '../shared/worktree/id'
import { parseWorkspaceKey } from '../shared/workspace-scope'
import { setMigrationUnsupportedPtyListener } from './agent-hooks/migration-unsupported-pty-state'
import { AgentBrowserBridge } from './browser/agent-browser-bridge'
+1 -1
View File
@@ -135,7 +135,7 @@ import {
import { listRepoWorktrees } from '../repo-worktrees'
import { recordCrashBreadcrumb } from '../crash-reporting/crash-breadcrumb-store'
import { buildReadDirErrorBreadcrumb, type ReadDirThrowSite } from './readdir-error-diagnostics'
import { splitWorktreeId } from '../../shared/worktree-id'
import { splitWorktreeId } from '../../shared/worktree/id'
import { getRuntimePathBasename } from '../../shared/cross-platform-path'
import type { LocalProjectWorktreeGitOptions } from '../project-runtime-git-options'
import { registerLocalLogTailHandlers } from './local-log-tail'
+1 -1
View File
@@ -107,7 +107,7 @@ import type {
UpdateIssueTypeBySlugArgs,
UpdateProjectItemFieldArgs,
UpdatePullRequestBySlugArgs
} from '../../shared/github-project-types'
} from '../../shared/github/project-types'
import { appStarSourceSchema } from '../../shared/gh-star-source'
import { track } from '../telemetry/client'
import { getCohortAtEmit } from '../telemetry/cohort-classifier'
+2 -2
View File
@@ -24,8 +24,8 @@ import {
} from '../linear/projects'
import { listTeams, getTeamStates, getTeamLabels, getTeamMembers } from '../linear/teams'
import type { LinearListFilter } from '../linear/issues'
import { clampLinearIssueListLimit } from '../../shared/linear-issue-read-limits'
import { optionalParsedLinearIssueAttributeFilter } from '../../shared/linear-issue-attribute-filter'
import { clampLinearIssueListLimit } from '../../shared/linear/issue-read-limits'
import { optionalParsedLinearIssueAttributeFilter } from '../../shared/linear/issue-attribute-filter'
import type {
LinearCustomViewModel,
LinearIssueUpdate,
@@ -4,7 +4,7 @@ import {
getLocalProjectWorktreeGitOptions,
type LocalProjectWorktreeGitOptions
} from '../project-runtime-git-options'
import { splitWorktreeId } from '../../shared/worktree-id'
import { splitWorktreeId } from '../../shared/worktree/id'
import type { Repo } from '../../shared/types'
function comparableLocalPath(value: string): string {
+1 -1
View File
@@ -21,7 +21,7 @@ import type {
NotificationSettings,
NotificationSoundDataResult
} from '../../shared/types'
import { getRepoIdFromWorktreeId } from '../../shared/worktree-id'
import { getRepoIdFromWorktreeId } from '../../shared/worktree/id'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import { buildNotificationOptions } from './notification-options'
import { readNotificationAuthorizationStatus } from './notification-authorization-status'
+1 -1
View File
@@ -148,7 +148,7 @@ import {
type TerminalStartupCwdMissingDirFallback
} from '../../shared/terminal-startup-cwd'
import { isWslUncPath, toWindowsWslPath } from '../../shared/wsl-paths'
import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id'
import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id'
import type { AgentSessionOwnerBinding } from '../../shared/agent-session-host-authority'
import {
agentSessionOwnerBindingsEqual,
+1 -1
View File
@@ -15,7 +15,7 @@ import type {
} from '../../shared/remote-workspace-types'
import type { SshTarget } from '../../shared/ssh-types'
import type { WorkspaceSessionState } from '../../shared/types'
import { getRepoIdFromWorktreeId } from '../../shared/worktree-id'
import { getRepoIdFromWorktreeId } from '../../shared/worktree/id'
import { getRemoteWorkspaceNamespace } from './remote-workspace-namespace'
import { registerRemoteWorkspaceNotificationHandler } from './remote-workspace-events'
+1 -1
View File
@@ -121,7 +121,7 @@ import { prepareLocalWorktreeRootForRepo } from '../worktree-root-preparation'
import {
normalizeCustomWorktreeVisibilitySources,
normalizeWorktreeVisibilitySourcePreferences
} from '../../shared/worktree-visibility-sources'
} from '../../shared/worktree/visibility-sources'
import { runWithGitReadCacheInvalidation } from '../git/status'
import { isAdmissibleDirectSshAuthority } from '../../shared/ssh-retained-payload-admission'
import { isCurrentSshProviderAuthority } from '../ssh/ssh-provider-authority'
@@ -2,7 +2,7 @@ import { resolve } from 'node:path'
import type { Store } from '../persistence'
import { isLinkedIssueNumber } from '../../shared/source-control-ai-action-variables'
import type { WorktreeMeta } from '../../shared/types'
import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id'
import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id'
export type LinkedIssueLookupArgs = {
worktreeId?: string
+1 -1
View File
@@ -6,7 +6,7 @@ import {
TerminalRemovalInProgressError,
WatcherRemovalInProgressError
} from './watcher-removal-gate'
import { isWorktreeRemovalFenceError } from '../../shared/worktree-removal-fence-error'
import { isWorktreeRemovalFenceError } from '../../shared/worktree/removal-fence-error'
describe('watcher removal gate', () => {
it('waits for an existing install and rejects later equivalent-path installs', async () => {
+1 -1
View File
@@ -5,7 +5,7 @@ import {
import {
TERMINAL_REMOVAL_IN_PROGRESS_MESSAGE,
WATCHER_REMOVAL_IN_PROGRESS_MESSAGE
} from '../../shared/worktree-removal-fence-error'
} from '../../shared/worktree/removal-fence-error'
// Why: fence slots are identities, not a counter, so a removal that gives up waiting can drop exactly
// the installs it waited on without a late finishInstall corrupting the count for a newer install.
@@ -6,7 +6,7 @@ import {
createWorkspaceCleanupFingerprint,
type WorkspaceCleanupCandidate
} from '../../shared/workspace-cleanup'
import { splitWorktreeId } from '../../shared/worktree-id'
import { splitWorktreeId } from '../../shared/worktree/id'
import {
getNewestWorkspaceCleanupDiffCommentAt,
getWorkspaceCleanupInactivityReasonsForWorkspace,
+1 -1
View File
@@ -5,7 +5,7 @@ import type { IGitProvider } from '../providers/types'
import { isFolderRepo } from '../../shared/repo-kind'
import type { GitWorktreeInfo, Repo, Worktree } from '../../shared/types'
import { mergeWorktree } from './worktree-logic'
import { splitWorktreeId } from '../../shared/worktree-id'
import { splitWorktreeId } from '../../shared/worktree/id'
import type {
WorkspaceCleanupCandidate,
WorkspaceCleanupScanError,
@@ -1,6 +1,6 @@
import { posix } from 'node:path'
import type { GlobalSettings } from '../../shared/types'
import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree-id'
import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree/id'
import { computeWorktreePath } from './worktree-logic'
type WorktreePathSettings = Pick<GlobalSettings, 'nestWorkspaces' | 'workspaceDir'>
@@ -1,6 +1,6 @@
import { resolveRuntimePath } from '../../shared/cross-platform-path'
import { isSafeGitStatusUpstreamRef } from '../../shared/git-status-upstream-ref'
import { getRepoIdFromWorktreeId } from '../../shared/worktree-id'
import { getRepoIdFromWorktreeId } from '../../shared/worktree/id'
import {
pathRelativeToWorktreeWatchRoot,
type WorktreeBaseWatchTarget
+1 -1
View File
@@ -2,7 +2,7 @@ import { resolve, relative, isAbsolute, posix, sep, win32 } from 'node:path'
import type { GlobalSettings, OrcaWorkspaceLayout, Repo } from '../../shared/types'
import { isWindowsAbsolutePathLike, resolveRuntimePath } from '../../shared/cross-platform-path'
import { isWslUncPath } from '../../shared/wsl-paths'
import { splitWorktreeId } from '../../shared/worktree-id'
import { splitWorktreeId } from '../../shared/worktree/id'
import { replaceKnownEmojiWithShortcodes } from '../../shared/emoji-shortcode-catalog'
import { getWslHome, parseWslPath } from '../wsl'
+1 -1
View File
@@ -6,7 +6,7 @@
import type { Store } from '../persistence'
import type { GitPushTarget } from '../../shared/types'
import { parseGitHubOwnerRepo } from '../github/gh-utils'
import { getRepoIdFromWorktreeId } from '../../shared/worktree-id'
import { getRepoIdFromWorktreeId } from '../../shared/worktree/id'
import { iterateProcessOutputLines } from '../../shared/process-output-field-scanner'
export type GitRemoteExec = (
+2 -2
View File
@@ -32,7 +32,7 @@ import {
import { resolveLocalGitUsername, getSshGitUsername } from '../git/git-username'
import { hasCommitObjectViaGitExec } from '../git/commit-object-ref'
import { resolveWorktreeCreateBase } from '../worktree-create-base'
import { resolveWorktreeAddBaseRef } from '../../shared/worktree-base-ref'
import { resolveWorktreeAddBaseRef } from '../../shared/worktree/base-ref'
import { getHostedReviewForBranch } from '../source-control/hosted-review'
import type { ForgeProviderId } from '../source-control/forge-provider'
import { validateGitPushTarget } from '../git/push-target-validation'
@@ -88,7 +88,7 @@ import {
} from './worktree-logic'
import { findCreatedWorktree } from './created-worktree-reconciliation'
import type { BranchPrefixSettings } from '../../shared/branch-prefix'
import { getRepoIdFromWorktreeId } from '../../shared/worktree-id'
import { getRepoIdFromWorktreeId } from '../../shared/worktree/id'
import { parseWorkspaceKey, worktreeWorkspaceKey } from '../../shared/workspace-scope'
import {
cleanupUnusedWorktreePushTargetRemoteWithExec,
+5 -5
View File
@@ -8,7 +8,7 @@ import { isFolderRepo } from '../../shared/repo-kind'
import { readBranchRenameFailureOutputForDisplay } from '../agent-hooks/branch-rename-failure-output'
import { parseWorkspaceKey } from '../../shared/workspace-scope'
import { inspectSetupScriptImportCandidates } from '../../shared/setup-script-imports'
import { planWorktreeSortOrderUpdates } from '../../shared/worktree-sort-order-update'
import { planWorktreeSortOrderUpdates } from '../../shared/worktree/sort-order-update'
import { getProjectHostSetupWorktreeMeta } from '../../shared/project-host-setup-projection'
import { TaskSourceContextSchema } from '../../shared/task-source-context-schema'
import { WorkspaceLinkedItemSchema } from '../../shared/workspace-linked-item-schema'
@@ -37,7 +37,7 @@ import type {
WorkspaceLineage,
WorktreeMeta
} from '../../shared/types'
import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree-removal'
import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree/removal'
import {
getRepoExecutionHostId,
LOCAL_EXECUTION_HOST_ID,
@@ -66,11 +66,11 @@ import {
buildKnownOrcaWorkspaceLayouts,
isLegacyRepoForExternalWorktreeVisibility,
toDetectedWorktree
} from '../../shared/worktree-ownership'
} from '../../shared/worktree/ownership'
import {
createWorktreeVisibilitySourceMatcher,
resolveCustomWorktreeVisibilitySources
} from '../../shared/worktree-visibility-sources'
} from '../../shared/worktree/visibility-sources'
import {
assertWorktreeCleanForRemoval,
forceDeleteLocalBranch,
@@ -239,7 +239,7 @@ import {
FOLDER_WORKSPACE_INSTANCE_SEPARATOR,
getRepoIdFromWorktreeId,
getWorktreePathBasenameFromId
} from '../../shared/worktree-id'
} from '../../shared/worktree/id'
import { prefetchWorktreeCreateBase } from '../worktree-create-base-prefetch'
import {
getLocalProjectGitExecOptions,
+1 -1
View File
@@ -1,7 +1,7 @@
import type {
LinearIssueActivityEntry,
LinearIssueActivityValue
} from '../../shared/linear-issue-activity'
} from '../../shared/linear/issue-activity'
import type { RawNamedEntity, RawPageInfo, RawUser } from './issue-context-raw'
type RawActivityEntity = RawNamedEntity & {
+2 -2
View File
@@ -1,5 +1,5 @@
import type { LinearSearchIssueSummary, LinearSearchResult } from '../../shared/linear-agent-access'
import { clampLinearSearchLimit } from '../../shared/linear-agent-access'
import type { LinearSearchIssueSummary, LinearSearchResult } from '../../shared/linear/agent-access'
import { clampLinearSearchLimit } from '../../shared/linear/agent-access'
import type { LinearWorkspace } from '../../shared/types'
import {
acquire,
+1 -1
View File
@@ -1,4 +1,4 @@
import { parseLinearIssueInput } from '../../shared/linear-links'
import { parseLinearIssueInput } from '../../shared/linear/links'
import { getConnectedWorkspaces } from './issue-context-client'
import { linearError } from './issue-context-errors'
+1 -1
View File
@@ -1,4 +1,4 @@
import type { LinearErrorCode, LinearIncludeErrorCode } from '../../shared/linear-agent-access'
import type { LinearErrorCode, LinearIncludeErrorCode } from '../../shared/linear/agent-access'
export class LinearAgentAccessError extends Error {
readonly code: LinearErrorCode
+1 -1
View File
@@ -1,4 +1,4 @@
import type { LinearErrorCode, LinearWorkspaceCandidate } from '../../shared/linear-agent-access'
import type { LinearErrorCode, LinearWorkspaceCandidate } from '../../shared/linear/agent-access'
import type { LinearWorkspace } from '../../shared/types'
import { getClients, getStatus, type LinearClientForWorkspace } from './client'
import {
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { LinearIssueContextResult, LinearIssueRequest } from '../../shared/linear-agent-access'
import type { LinearIssueContextResult, LinearIssueRequest } from '../../shared/linear/agent-access'
import type { ResolvedIssue } from './issue-context-client'
import { ACTIVITY_QUERY } from './issue-activity-raw'
import {
+3 -3
View File
@@ -7,7 +7,7 @@ import type {
LinearIssueContextResult,
LinearIssueInclude,
LinearIssueRequest
} from '../../shared/linear-agent-access'
} from '../../shared/linear/agent-access'
import {
LINEAR_ATTACHMENTS_CAP,
LINEAR_ACTIVITY_CAP,
@@ -15,8 +15,8 @@ import {
LINEAR_COMMENTS_CAP,
LINEAR_COMMENT_BODY_CAP,
clampLinearIssueDepth
} from '../../shared/linear-agent-access'
import { extractLinearInlineMedia } from '../../shared/linear-inline-media'
} from '../../shared/linear/agent-access'
import { extractLinearInlineMedia } from '../../shared/linear/inline-media'
import type { ResolvedIssue } from './issue-context-client'
import { getRequiredEntry, withLinearRead } from './issue-context-client'
import { getPublicFileUrlClient } from './client'
@@ -4,7 +4,7 @@ import type {
LinearIssueCommentNode,
LinearIssueContextResult,
LinearIssueSummary
} from '../../shared/linear-agent-access'
} from '../../shared/linear/agent-access'
import { collectInlineMedia } from './issue-context'
function summary(overrides: Partial<LinearIssueSummary>): LinearIssueSummary {
+1 -1
View File
@@ -1,4 +1,4 @@
import { LINEAR_ISSUE_API_PAGE_SIZE_MAX } from '../../shared/linear-issue-read-limits'
import { LINEAR_ISSUE_API_PAGE_SIZE_MAX } from '../../shared/linear/issue-read-limits'
export type LinearPageVariables = { first: number; after?: string }
+1 -1
View File
@@ -2,7 +2,7 @@ import type {
LinearCollectionMeta,
LinearIssueSummary,
LinearSearchIssueSummary
} from '../../shared/linear-agent-access'
} from '../../shared/linear/agent-access'
export type RawIssueResponse = {
issue?: RawIssue | null
+2 -2
View File
@@ -1,5 +1,5 @@
import type { LinearCollectionMeta, LinearIssueRelation } from '../../shared/linear-agent-access'
import { LINEAR_RELATIONS_CAP } from '../../shared/linear-agent-access'
import type { LinearCollectionMeta, LinearIssueRelation } from '../../shared/linear/agent-access'
import { LINEAR_RELATIONS_CAP } from '../../shared/linear/agent-access'
import type { ResolvedIssue } from './issue-context-client'
import { getRequiredEntry, withLinearRead } from './issue-context-client'
import { readConnectionPages } from './issue-context-pagination'
+1 -1
View File
@@ -1,4 +1,4 @@
import type { LinearWorkspaceCandidate } from '../../shared/linear-agent-access'
import type { LinearWorkspaceCandidate } from '../../shared/linear/agent-access'
import type { LinearWorkspace } from '../../shared/types'
import { linearError } from './issue-context-errors'
+3 -3
View File
@@ -3,9 +3,9 @@ import type {
LinearIssueChildNode,
LinearIssueContextResult,
LinearIssueRequest
} from '../../shared/linear-agent-access'
import { extractLinearInlineMedia } from '../../shared/linear-inline-media'
import { parseLinearIssueInput } from '../../shared/linear-links'
} from '../../shared/linear/agent-access'
import { extractLinearInlineMedia } from '../../shared/linear/inline-media'
import { parseLinearIssueInput } from '../../shared/linear/links'
import {
resolveIssue,
searchLinearIssuesForAgents,
+1 -1
View File
@@ -1,7 +1,7 @@
import {
isEmptyLinearIssueAttributeFilter,
type LinearIssueAttributeFilter
} from '../../shared/linear-issue-attribute-filter'
} from '../../shared/linear/issue-attribute-filter'
import type { LinearListFilter } from './issues'
// Why: attribute facets must merge into Linear's IssueFilter before the first-N
+2 -2
View File
@@ -3,8 +3,8 @@ import { loadLinearSdk } from './linear-sdk'
import type {
LinearIssueRelationship,
LinearIssueRelationWriteResult
} from '../../shared/linear-issue-relation-write'
import { LINEAR_ISSUE_API_PAGE_SIZE_MAX } from '../../shared/linear-issue-read-limits'
} from '../../shared/linear/issue-relation-write'
import { LINEAR_ISSUE_API_PAGE_SIZE_MAX } from '../../shared/linear/issue-read-limits'
import { acquire, clearToken, getClients, isAuthError, release } from './client'
import { linearError } from './issue-context-errors'
import {
+2 -2
View File
@@ -14,11 +14,11 @@ import { loadLinearSdk } from './linear-sdk'
import {
LINEAR_ISSUE_API_PAGE_SIZE_MAX,
clampLinearIssueListLimit
} from '../../shared/linear-issue-read-limits'
} from '../../shared/linear/issue-read-limits'
import {
isEmptyLinearIssueAttributeFilter,
type LinearIssueAttributeFilter
} from '../../shared/linear-issue-attribute-filter'
} from '../../shared/linear/issue-attribute-filter'
import {
acquire,
release,
+1 -1
View File
@@ -1,5 +1,5 @@
import type { LinearLabel, LinearMember, LinearTeam, LinearWorkflowState } from '../../shared/types'
import { buildLinearTeamUrl } from '../../shared/linear-links'
import { buildLinearTeamUrl } from '../../shared/linear/links'
import type { LinearClientForWorkspace } from './client'
const TEAM_PAGE_SIZE = 100
+1 -1
View File
@@ -1,7 +1,7 @@
import type {
LinearMcpIssueListRequest,
LinearMcpIssueListResult
} from '../../shared/linear-agent-access'
} from '../../shared/linear/agent-access'
import { getClients, getStatus, type LinearClientForWorkspace } from './client'
import { withLinearRead } from './issue-context-client'
import { linearError } from './issue-context-errors'
+1 -1
View File
@@ -15,7 +15,7 @@ import type {
import {
LINEAR_ISSUE_API_PAGE_SIZE_MAX,
clampLinearIssueListLimit
} from '../../shared/linear-issue-read-limits'
} from '../../shared/linear/issue-read-limits'
import {
acquire,
clearToken,
+1 -1
View File
@@ -10,7 +10,7 @@ import {
hasCachedWslAvailability,
hasCachedWslDistros
} from './wsl'
import { getRepoIdFromWorktreeId } from '../shared/worktree-id'
import { getRepoIdFromWorktreeId } from '../shared/worktree/id'
import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../shared/execution-host'
function canResolveProjectRuntimeForRepo(store: Store): boolean {
+1 -1
View File
@@ -1,5 +1,5 @@
import type { GitWorktreeInfo, RemoveWorktreeResult } from '../shared/types'
import { assertWorktreeUnlockedForRemoval } from '../shared/worktree-removal'
import { assertWorktreeUnlockedForRemoval } from '../shared/worktree/removal'
import { areWorktreePathsEqual, formatWorktreeRemovalError } from './ipc/worktree-logic'
import { gitExecFileAsync } from './git/runner'
import { listWorktreesStrict, type GitWorktreeExecOptions } from './git/worktree'
+1 -1
View File
@@ -21,7 +21,7 @@ import { basename } from 'node:path'
import { exec } from 'node:child_process'
import { promisify } from 'node:util'
import os from 'node:os'
import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id'
import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id'
import {
getProcessOutputFields,
iterateProcessOutputLines
@@ -16,7 +16,7 @@ import type { SessionInfo } from '../daemon/types'
import { listRegisteredPtys, registerPty } from './pty-registry'
import { listRepoWorktrees } from '../repo-worktrees'
import { parsePtySessionId } from '../../shared/pty-session-id-format'
import { splitWorktreeId } from '../../shared/worktree-id'
import { splitWorktreeId } from '../../shared/worktree/id'
import type { Store } from '../persistence'
// Why: macOS dock reactivation reruns setup; avoid repeating Git I/O and daemon RPC.
@@ -130,4 +130,48 @@ describe('Codex WSL scan gate', () => {
})
).rejects.toBe(refusal)
})
it('reports the abort, not a refusal, when the hook-path probe races a caller abort', async () => {
const controller = new AbortController()
const abortReason = new Error('caller went away')
mocks.gate.mockImplementation(async (options: { operation?: string; path: string }) => {
if (options.operation === 'access') {
controller.abort(abortReason)
throw new WslTranscriptFsError('timeout', 'slow share')
}
return []
})
await expect(
resolveSessionFilePath(
'codex',
'session-id',
{
transcriptPath: `${WSL_SESSIONS_DIR}\\2026\\rollout-1-session-id.jsonl`,
codexSessionsDirs: [LOCAL_SESSIONS_DIR]
},
controller.signal
)
).rejects.toBe(abortReason)
})
// The scan layer's own waiter already wins this race; the resolver's post-catch
// signal check is the backstop if that ever stops holding.
it('reports the abort, not a refusal, when a scanned root is aborted mid-scan', async () => {
const controller = new AbortController()
const abortReason = new Error('caller went away')
mocks.gate.mockImplementation(async () => {
controller.abort(abortReason)
throw new WslTranscriptFsError('timeout', 'slow share')
})
await expect(
resolveSessionFilePath(
'codex',
'session-id',
{ codexSessionsDirs: [WSL_SESSIONS_DIR] },
controller.signal
)
).rejects.toBe(abortReason)
})
})
@@ -108,6 +108,8 @@ export async function resolveSessionFilePath(
return hostReadable
}
} catch (error) {
// A caller abort that races the refusal stays authoritative.
signal?.throwIfAborted()
// Why: the id-based search may still hit; surface the refusal only when
// it does not, so a stalled distro reads as unavailable, never "missing".
unavailable = wslTranscriptFsRefusal(error)
@@ -232,6 +234,8 @@ async function findCodexRolloutInDirs(
return files
}
} catch (error) {
// A caller abort that races the refusal stays authoritative.
signal?.throwIfAborted()
// Why: one stalled distro must not hide another root's hit.
unavailable = wslTranscriptFsRefusal(error)
}
@@ -0,0 +1,153 @@
import { describe, expect, it } from 'vitest'
import { getDefaultPersistedState } from '../../shared/constants'
import type { PersistedState, Project, ProjectHostSetup, Repo } from '../../shared/types'
import { rebuildRepoBackedProjectState } from './profile-project-state-file'
const upstreamIdentity = {
canonicalKey: 'git.example.com/acme/app-upstream',
remoteName: 'upstream',
remoteUrl: 'git@git.example.com:acme/app-upstream.git'
}
const makeRepo = (overrides: Partial<Repo> = {}): Repo => ({
id: 'r1',
path: '/repo',
displayName: 'App',
badgeColor: '#fff',
addedAt: 1,
...overrides
})
const makeProject = (overrides: Partial<Project> = {}): Project => ({
id: 'project-1',
displayName: 'App',
badgeColor: '#737373',
sourceRepoIds: [],
createdAt: 1,
updatedAt: 1,
...overrides
})
const makeSetup = (overrides: Partial<ProjectHostSetup> = {}): ProjectHostSetup => ({
id: 'setup-1',
projectId: 'project-1',
hostId: 'local',
repoId: '',
path: '/repo',
displayName: 'App',
setupState: 'ready',
setupMethod: 'imported-existing-folder',
createdAt: 1,
updatedAt: 1,
...overrides
})
function makeState(overrides: Partial<PersistedState>): PersistedState {
return { ...getDefaultPersistedState('/home/test'), ...overrides }
}
describe('rebuildRepoBackedProjectState', () => {
it('carries project state and independent setups across a repo remote identity change', () => {
const originProjectId = 'git:git.example.com/acme/app'
const rebuilt = rebuildRepoBackedProjectState(
makeState({
repos: [makeRepo({ gitRemoteIdentity: upstreamIdentity })],
projects: [
makeProject({
id: originProjectId,
sourceRepoIds: ['r1'],
localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }
})
],
projectHostSetups: [
makeSetup({ id: 'r1', projectId: originProjectId, repoId: 'r1' }),
makeSetup({
id: 'app::gpu-vm',
projectId: originProjectId,
hostId: 'runtime:gpu-vm',
path: '/srv/app'
})
]
})
)
expect(rebuilt.projects).toEqual([
expect.objectContaining({
id: 'git:git.example.com/acme/app-upstream',
localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }
})
])
expect(rebuilt.projectHostSetups.find((setup) => setup.id === 'app::gpu-vm')?.projectId).toBe(
'git:git.example.com/acme/app-upstream'
)
})
it('picks one predecessor project when several prior rows overlap the same repos', () => {
const sharedIdentity = {
canonicalKey: 'git.example.com/acme/shared',
remoteName: 'origin',
remoteUrl: 'git@git.example.com:acme/shared.git'
}
const rebuilt = rebuildRepoBackedProjectState(
makeState({
repos: [
makeRepo({ id: 'r1', path: '/left', gitRemoteIdentity: sharedIdentity }),
makeRepo({ id: 'r2', path: '/right', gitRemoteIdentity: sharedIdentity })
],
projects: [
makeProject({
id: 'git:git.example.com/acme/left',
sourceRepoIds: ['r1'],
updatedAt: 200,
localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }
}),
makeProject({
id: 'git:git.example.com/acme/right',
sourceRepoIds: ['r2'],
updatedAt: 100,
localWindowsRuntimePreference: { kind: 'windows-host' }
})
],
projectHostSetups: [
makeSetup({ id: 'r1', projectId: 'git:git.example.com/acme/left', repoId: 'r1' }),
makeSetup({ id: 'r2', projectId: 'git:git.example.com/acme/right', repoId: 'r2' })
]
})
)
// Equal repo overlap resolves by newest updatedAt; the loser's preference is never merged in.
expect(rebuilt.projects).toEqual([
expect.objectContaining({
id: 'git:git.example.com/acme/shared',
sourceRepoIds: ['r1', 'r2'],
localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }
})
])
})
it('leaves an unclaimed prior project standing with its own independent setups', () => {
const rebuilt = rebuildRepoBackedProjectState(
makeState({
repos: [makeRepo({ gitRemoteIdentity: upstreamIdentity })],
projects: [
makeProject({
id: 'cloud-project',
localWindowsRuntimePreference: { kind: 'windows-host' }
})
],
projectHostSetups: [
makeSetup({ id: 'cloud-project::gpu-vm', projectId: 'cloud-project', path: '/srv/cloud' })
]
})
)
expect(rebuilt.projects.map((project) => project.id)).toEqual([
'git:git.example.com/acme/app-upstream',
'cloud-project'
])
expect(rebuilt.projectHostSetups.map((setup) => setup.projectId)).toEqual([
'git:git.example.com/acme/app-upstream',
'cloud-project'
])
})
})
@@ -5,6 +5,7 @@ import { dirname } from 'node:path'
import { getDefaultPersistedState, getDefaultWorkspaceSession } from '../../shared/constants'
import type { ExecutionHostId } from '../../shared/execution-host'
import { projectHostSetupProjectionFromRepos } from '../../shared/project-host-setup-projection'
import { carryProjectStateThroughIdentityChange } from '../../shared/project-identity-succession'
import type {
PersistedState,
Project,
@@ -102,16 +103,22 @@ function isRepoBackedProjectHostSetup(
export function rebuildRepoBackedProjectState(state: TransferProfileState): TransferProfileState {
const projection = projectHostSetupProjectionFromRepos(state.repos)
const existingProjectsById = new Map(state.projects.map((project) => [project.id, project]))
const succession = carryProjectStateThroughIdentityChange(projection.projects, state.projects)
const currentRepoIds = new Set(state.repos.map((repo) => repo.id))
const projectedProjectIds = new Set(projection.projects.map((project) => project.id))
const projectedSetupIds = new Set(projection.setups.map((setup) => setup.id))
const independentSetups = state.projectHostSetups.filter((setup) => {
if (projectedSetupIds.has(setup.id)) {
return false
}
return !isRepoBackedProjectHostSetup(setup, currentRepoIds)
})
const independentSetups = state.projectHostSetups
.filter((setup) => {
if (projectedSetupIds.has(setup.id)) {
return false
}
return !isRepoBackedProjectHostSetup(setup, currentRepoIds)
})
// Why: follow the repo's project through a derived-id change so no ghost project row survives.
.map((setup) => {
const remappedProjectId = succession.remappedProjectIds.get(setup.projectId)
return remappedProjectId ? { ...setup, projectId: remappedProjectId } : setup
})
const independentProjectIds = new Set(independentSetups.map((setup) => setup.projectId))
const independentProjects = state.projects
.filter(
@@ -121,19 +128,9 @@ export function rebuildRepoBackedProjectState(state: TransferProfileState): Tran
...project,
sourceRepoIds: project.sourceRepoIds.filter((repoId) => currentRepoIds.has(repoId))
}))
const projectedProjects = projection.projects.map((project) => {
const existingProject = existingProjectsById.get(project.id)
return existingProject?.localWindowsRuntimePreference
? {
...project,
localWindowsRuntimePreference: existingProject.localWindowsRuntimePreference,
updatedAt: Math.max(project.updatedAt, existingProject.updatedAt)
}
: project
})
return {
...state,
projects: [...projectedProjects, ...independentProjects],
projects: [...succession.projects, ...independentProjects],
projectHostSetups: [...projection.setups, ...independentSetups]
}
}
@@ -2,7 +2,7 @@ import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-p
import { getRepoExecutionHostId } from '../../shared/execution-host'
import type { Repo, WorkspaceKey } from '../../shared/types'
import { parseWorkspaceKey, worktreeWorkspaceKey } from '../../shared/workspace-scope'
import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree-id'
import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree/id'
export function repoPhysicalKey(
repo: Pick<Repo, 'path' | 'connectionId' | 'executionHostId'>
+118
View File
@@ -595,6 +595,124 @@ describe('Store', () => {
})
})
it('carries project state and independent setups across a repo remote identity change', async () => {
const originProjectId = 'git:git.example.com/acme/app'
writeDataFile({
...getDefaultPersistedState(testState.dir),
repos: [
makeRepo({
id: 'r1',
path: '/repo',
displayName: 'App',
gitRemoteIdentity: {
canonicalKey: 'git.example.com/acme/app',
remoteName: 'origin',
remoteUrl: 'git@git.example.com:acme/app.git'
}
})
],
projects: [
makeProject({
id: originProjectId,
sourceRepoIds: ['r1'],
localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }
})
],
projectHostSetups: [
makeProjectHostSetup({ id: 'r1', projectId: originProjectId, repoId: 'r1' }),
makeProjectHostSetup({
id: 'app::gpu-vm',
projectId: originProjectId,
hostId: 'runtime:gpu-vm',
repoId: '',
path: '/srv/app'
})
]
})
const store = await createStore()
// A re-probe that now prefers the `upstream` remote rewrites the derived project id.
store.updateRepo('r1', {
gitRemoteIdentity: {
canonicalKey: 'git.example.com/acme/app-upstream',
remoteName: 'upstream',
remoteUrl: 'git@git.example.com:acme/app-upstream.git'
}
})
const upstreamProjectId = 'git:git.example.com/acme/app-upstream'
expect(store.getProjects().map((project) => project.id)).toEqual([upstreamProjectId])
expect(store.getProjects()[0]?.localWindowsRuntimePreference).toEqual({
kind: 'wsl',
distro: 'Ubuntu'
})
expect(
store.getProjectHostSetups().find((setup) => setup.id === 'app::gpu-vm')?.projectId
).toBe(upstreamProjectId)
})
it('picks one predecessor project when several prior rows overlap the same repos', async () => {
const sharedIdentity = {
canonicalKey: 'git.example.com/acme/shared',
remoteName: 'origin',
remoteUrl: 'git@git.example.com:acme/shared.git'
}
writeDataFile({
...getDefaultPersistedState(testState.dir),
repos: [
makeRepo({
id: 'r1',
path: '/left',
displayName: 'Left',
gitRemoteIdentity: sharedIdentity
}),
makeRepo({
id: 'r2',
path: '/right',
displayName: 'Right',
gitRemoteIdentity: sharedIdentity
})
],
projects: [
makeProject({
id: 'git:git.example.com/acme/left',
sourceRepoIds: ['r1'],
updatedAt: 200,
localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }
}),
makeProject({
id: 'git:git.example.com/acme/right',
sourceRepoIds: ['r2'],
updatedAt: 100,
localWindowsRuntimePreference: { kind: 'windows-host' }
})
],
projectHostSetups: [
makeProjectHostSetup({
id: 'r1',
projectId: 'git:git.example.com/acme/left',
repoId: 'r1'
}),
makeProjectHostSetup({
id: 'r2',
projectId: 'git:git.example.com/acme/right',
repoId: 'r2'
})
]
})
const store = await createStore()
// Equal repo overlap resolves by newest updatedAt; the loser's preference is never merged in.
expect(store.getProjects()).toEqual([
expect.objectContaining({
id: 'git:git.example.com/acme/shared',
sourceRepoIds: ['r1', 'r2'],
localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }
})
])
})
it('migrates legacy WSL agent settings into the global Windows runtime default', async () => {
writeDataFile({
schemaVersion: 1,
+20 -22
View File
@@ -79,6 +79,7 @@ import {
normalizeProjectRuntimePreference
} from '../shared/project-execution-runtime'
import { projectHostSetupProjectionFromRepos } from '../shared/project-host-setup-projection'
import { carryProjectStateThroughIdentityChange } from '../shared/project-identity-succession'
import { isPluginPanelTabKey } from '../shared/plugins/plugin-manifest'
import type { GitRemoteIdentity } from '../shared/git-remote-identity'
import {
@@ -140,7 +141,7 @@ import { normalizeStatusBarUsageMode } from '../shared/status-bar-usage-mode'
import {
normalizeCustomWorktreeVisibilitySources,
normalizeWorktreeVisibilitySourcePreferences
} from '../shared/worktree-visibility-sources'
} from '../shared/worktree/visibility-sources'
import { isExistingPersistedProfile } from '../shared/project-order-manual-default-notice'
import { resolveUsagePercentageDisplayChangeNoticeDismissed } from '../shared/usage-percentage-display-change-notice'
import { normalizePRBotAuthorOverrides } from '../shared/pr-bot-author-overrides'
@@ -172,7 +173,7 @@ import {
FOLDER_WORKSPACE_INSTANCE_SEPARATOR,
getRepoIdFromWorktreeId,
getWorktreePathBasenameFromId
} from '../shared/worktree-id'
} from '../shared/worktree/id'
import {
isPathInsideOrEqual,
isWindowsAbsolutePathLike,
@@ -219,7 +220,7 @@ import {
} from '../shared/workspace-statuses'
import { clampMarkdownTocPanelWidth } from '../shared/markdown-toc-panel-width'
import { clampCombinedDiffFileTreeWidth } from '../shared/combined-diff-file-tree-width'
import { isLegacyRepoForExternalWorktreeVisibility } from '../shared/worktree-ownership'
import { isLegacyRepoForExternalWorktreeVisibility } from '../shared/worktree/ownership'
import {
migrateExternalWorktreeVisibilityDefaults,
normalizeWorktreeVisibilityDefaults
@@ -2470,19 +2471,26 @@ function mergeProjectHostSetupCompatibilityState(
repos: readonly Repo[]
): Pick<PersistedState, 'projects' | 'projectHostSetups'> {
const projection = projectHostSetupProjectionFromRepos(repos)
const existingProjectsById = new Map(
(state.projects ?? []).map((project) => [project.id, project])
const succession = carryProjectStateThroughIdentityChange(
projection.projects,
state.projects ?? []
)
const currentRepoIds = new Set(repos.map((repo) => repo.id))
const projectedProjectIds = new Set(projection.projects.map((project) => project.id))
const projectedSetupIds = new Set(projection.setups.map((setup) => setup.id))
// Why: legacy/repo-backed setup rows reuse the repo id; keep only independent rows so repo deletion leaves no ghosts.
const independentSetups = (state.projectHostSetups ?? []).filter((setup) => {
if (projectedSetupIds.has(setup.id)) {
return false
}
return !isRepoBackedProjectHostSetup(setup, currentRepoIds)
})
const independentSetups = (state.projectHostSetups ?? [])
.filter((setup) => {
if (projectedSetupIds.has(setup.id)) {
return false
}
return !isRepoBackedProjectHostSetup(setup, currentRepoIds)
})
// Why: follow the repo's project through a derived-id change so no ghost project row survives.
.map((setup) => {
const remappedProjectId = succession.remappedProjectIds.get(setup.projectId)
return remappedProjectId ? { ...setup, projectId: remappedProjectId } : setup
})
const independentProjectIds = new Set(independentSetups.map((setup) => setup.projectId))
const independentProjects = (state.projects ?? [])
.filter(
@@ -2492,18 +2500,8 @@ function mergeProjectHostSetupCompatibilityState(
...project,
sourceRepoIds: project.sourceRepoIds.filter((repoId) => currentRepoIds.has(repoId))
}))
const projectedProjects = projection.projects.map((project) => {
const existingProject = existingProjectsById.get(project.id)
return existingProject?.localWindowsRuntimePreference
? {
...project,
localWindowsRuntimePreference: existingProject.localWindowsRuntimePreference,
updatedAt: Math.max(project.updatedAt, existingProject.updatedAt)
}
: project
})
return {
projects: [...projectedProjects, ...independentProjects],
projects: [...succession.projects, ...independentProjects],
projectHostSetups: [...projection.setups, ...independentSetups]
}
}
@@ -7,7 +7,7 @@
import type { DetectedPort, EnrichedDetectedPort, PortForwardEntry } from '../../shared/ssh-types'
import type { Store } from '../persistence'
import { splitWorktreeId } from '../../shared/worktree-id'
import { splitWorktreeId } from '../../shared/worktree/id'
import {
advertisedUrlWatcher,
type AdvertisedUrl,
+1 -1
View File
@@ -1,6 +1,6 @@
import path from 'node:path'
import type { Store } from '../persistence'
import { splitWorktreeId, splitWorktreeIdForFilesystem } from '../../shared/worktree-id'
import { splitWorktreeId, splitWorktreeIdForFilesystem } from '../../shared/worktree/id'
import { isFolderRepo } from '../../shared/repo-kind'
import type {
WorkspacePortKillRequest,

Some files were not shown because too many files have changed in this diff Show More