mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 16:03:26 +00:00
refactor(renderer): make the store repos array readonly at the type level
Preserving the repos array identity means handing callers the same array that is live store state, which previously relied on `as Repo[]` casts to launder readonly inputs back into a mutable field. A cast is a footgun: the next person to add a .push or .sort corrupts store state with no type error. Widen RepoSlice['repos'] to readonly Repo[] and propagate honestly. Consumers that only read take readonly Repo[]; genuine local accumulators are annotated Repo[] and built from copies. Removes all four pre-existing `as Repo[]` casts in the reconcile chain (repo-identity-reconcile, superseded-ssh-repo-rows, manual-repo-order x2) — this lands with fewer casts than main has today. Type-only change; no runtime behavior differs. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -149,7 +149,7 @@ function PreflightBanner({
|
||||
repos
|
||||
}: {
|
||||
issues: PreflightIssue[]
|
||||
repos: Repo[]
|
||||
repos: readonly Repo[]
|
||||
}): React.JSX.Element | null {
|
||||
// Why: keying the seed on the current GitHub project set means adding a new
|
||||
// GitHub project (which changes the key) re-evaluates dismissals, so a lapsed
|
||||
|
||||
@@ -84,7 +84,7 @@ type NewWorkspaceComposerCardProps = {
|
||||
nameInputRef?: React.RefObject<HTMLInputElement | null>
|
||||
quickAgent: TuiAgent | null
|
||||
onQuickAgentChange: (agent: TuiAgent | null) => void
|
||||
eligibleRepos: RepoOption[]
|
||||
eligibleRepos: readonly RepoOption[]
|
||||
repoId: string
|
||||
projectOptions?: NewWorkspaceProjectOption[]
|
||||
selectedProjectId?: string | null
|
||||
@@ -98,7 +98,7 @@ type NewWorkspaceComposerCardProps = {
|
||||
selectedEphemeralVmRecipeId?: string | null
|
||||
onEphemeralVmRecipeChange?: (recipeId: string | null) => void
|
||||
ephemeralVmRecipeError?: string | null
|
||||
repoBackedSearchRepos?: RepoOption[]
|
||||
repoBackedSearchRepos?: readonly RepoOption[]
|
||||
repoBackedSourcesDisabled?: boolean
|
||||
allowSmartNameAddProject?: boolean
|
||||
smartNameRepoSwitchTarget?: 'project' | 'task-source'
|
||||
|
||||
@@ -61,7 +61,7 @@ type AutomationEditorDialogProps = {
|
||||
isSaving: boolean
|
||||
canSave: boolean
|
||||
createTarget: AutomationCreateTarget
|
||||
repos: Repo[]
|
||||
repos: readonly Repo[]
|
||||
projectHostSetups: ProjectHostSetup[]
|
||||
automationYamlHooksByRepoKey: Record<string, OrcaHooks | null>
|
||||
getAutomationHooksCacheKey: (repoId: string) => string
|
||||
|
||||
@@ -31,7 +31,7 @@ type AutomationEditorDialogFooterProps = {
|
||||
isHermesCreate: boolean
|
||||
isSaving: boolean
|
||||
canSave: boolean
|
||||
repos: Repo[]
|
||||
repos: readonly Repo[]
|
||||
projectHostSetups: ProjectHostSetup[]
|
||||
automationYamlHooksByRepoKey: Record<string, OrcaHooks | null>
|
||||
getAutomationHooksCacheKey: (repoId: string) => string
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from './automation-project-groups'
|
||||
|
||||
type AutomationProjectComboboxProps = {
|
||||
repos: Repo[]
|
||||
repos: readonly Repo[]
|
||||
value: string
|
||||
onValueChange: (repoId: string) => void
|
||||
placeholder?: string
|
||||
|
||||
@@ -94,8 +94,22 @@ describe('buildDashboardWorktreeLaunchOptions', () => {
|
||||
const options = buildDashboardWorktreeLaunchOptions(
|
||||
state({
|
||||
repos: [
|
||||
{ id: 'repo-ssh', connectionId: 'ssh-1' },
|
||||
{ id: 'repo-runtime', executionHostId: 'runtime:hub-1' }
|
||||
{
|
||||
id: 'repo-ssh',
|
||||
path: '/repo-ssh',
|
||||
displayName: 'repo-ssh',
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1,
|
||||
connectionId: 'ssh-1'
|
||||
},
|
||||
{
|
||||
id: 'repo-runtime',
|
||||
path: '/repo-runtime',
|
||||
displayName: 'repo-runtime',
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1,
|
||||
executionHostId: 'runtime:hub-1'
|
||||
}
|
||||
] as LaunchState['repos'],
|
||||
worktreesByRepo: {
|
||||
'repo-ssh': [{ id: 'ssh-worktree', repoId: 'repo-ssh' }],
|
||||
|
||||
@@ -22,7 +22,7 @@ import { parsePaneKey } from '../../../../shared/stable-pane-id'
|
||||
type RetainedAgentSnapshot = Map<string, { row: DashboardAgentRow; worktreeId: string }>
|
||||
|
||||
type RetainedAgentsSyncInputs = {
|
||||
repos: Repo[]
|
||||
repos: readonly Repo[]
|
||||
worktreesByRepo: Record<string, Worktree[]>
|
||||
folderWorkspaces: FolderWorkspace[]
|
||||
tabsByWorktree: Record<string, TerminalTab[]>
|
||||
@@ -38,7 +38,7 @@ function paneKeyTabId(paneKey: string): string | null {
|
||||
}
|
||||
|
||||
function buildLiveTabIndex(args: {
|
||||
repos: Repo[]
|
||||
repos: readonly Repo[]
|
||||
worktreesByRepo: Record<string, Worktree[]>
|
||||
folderWorkspaces: FolderWorkspace[]
|
||||
tabsByWorktree: Record<string, TerminalTab[]>
|
||||
|
||||
@@ -23,7 +23,7 @@ function storageKey(issueId: string): string {
|
||||
|
||||
/** GitHub-backed project identity keys for the current repo set, de-duped so
|
||||
* the same GitHub project added twice doesn't read as two distinct projects. */
|
||||
export function githubProjectKeys(repos: Repo[]): string[] {
|
||||
export function githubProjectKeys(repos: readonly Repo[]): string[] {
|
||||
const keys = repos
|
||||
.filter((repo) => isGitHubBackedRepo(repo))
|
||||
.map((repo) => getProjectIdentityKey(repo))
|
||||
@@ -45,7 +45,7 @@ function readRecord(issueId: string): DismissalRecord | null {
|
||||
|
||||
/** True when the issue was dismissed and no new GitHub project has appeared
|
||||
* since. A GitHub key present now but absent from the snapshot re-surfaces it. */
|
||||
export function isPreflightIssueDismissed(issueId: string, repos: Repo[]): boolean {
|
||||
export function isPreflightIssueDismissed(issueId: string, repos: readonly Repo[]): boolean {
|
||||
const record = readRecord(issueId)
|
||||
if (!record) {
|
||||
return false
|
||||
@@ -55,7 +55,7 @@ export function isPreflightIssueDismissed(issueId: string, repos: Repo[]): boole
|
||||
return !hasNewGithubProject
|
||||
}
|
||||
|
||||
export function dismissPreflightIssue(issueId: string, repos: Repo[]): void {
|
||||
export function dismissPreflightIssue(issueId: string, repos: readonly Repo[]): void {
|
||||
try {
|
||||
const record: DismissalRecord = { githubKeys: githubProjectKeys(repos) }
|
||||
localStorage.setItem(storageKey(issueId), JSON.stringify(record))
|
||||
|
||||
@@ -114,7 +114,7 @@ type RepoOption = ReturnType<typeof useAppStore.getState>['repos'][number]
|
||||
const EMPTY_REPO_SEARCH_REPOS: readonly RepoOption[] = []
|
||||
|
||||
type SmartWorkspaceNameFieldProps = {
|
||||
repos: RepoOption[]
|
||||
repos: readonly RepoOption[]
|
||||
repoId: string
|
||||
onRepoChange: (repoId: string) => void
|
||||
value: string
|
||||
|
||||
@@ -19,7 +19,7 @@ import RepoBadgeLabel from './RepoBadgeLabel'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
type RepoComboboxProps = {
|
||||
repos: Repo[]
|
||||
repos: readonly Repo[]
|
||||
value: string
|
||||
onValueChange: (repoId: string) => void
|
||||
onValueSelected?: (repoId: string) => void
|
||||
|
||||
@@ -217,7 +217,7 @@ describe('useGitStatusPolling rerender stability', () => {
|
||||
useAppStore.setState({
|
||||
repos: [{ ...repo, connectionId: 'ssh-1' }],
|
||||
sshConnectionStates: new Map([
|
||||
['ssh-1', { status: 'disconnected', error: null, reconnectAttempt: 0 }]
|
||||
['ssh-1', { targetId: 'ssh-1', status: 'disconnected', error: null, reconnectAttempt: 0 }]
|
||||
])
|
||||
} as Partial<AppState>)
|
||||
await renderHook()
|
||||
|
||||
@@ -21,7 +21,7 @@ function ScopeTriggerLabel({
|
||||
}: {
|
||||
showAll: boolean
|
||||
effectiveSelection: ReadonlySet<string>
|
||||
repos: Repo[]
|
||||
repos: readonly Repo[]
|
||||
}): React.JSX.Element {
|
||||
if (showAll) {
|
||||
return (
|
||||
@@ -57,7 +57,7 @@ export function QuickCommandsScopeFilter({
|
||||
handleSelectAll,
|
||||
toggleScope
|
||||
}: {
|
||||
repos: Repo[]
|
||||
repos: readonly Repo[]
|
||||
effectiveSelection: ReadonlySet<string>
|
||||
showAll: boolean
|
||||
scopePopoverOpen: boolean
|
||||
|
||||
@@ -117,7 +117,16 @@ describe('resource usage open slices', () => {
|
||||
})
|
||||
|
||||
it('gates repo and worktree slices only while closed', () => {
|
||||
const repos = [{ id: 'repo-1', path: '/repo', kind: 'git' }] as AppState['repos']
|
||||
const repos = [
|
||||
{
|
||||
id: 'repo-1',
|
||||
path: '/repo',
|
||||
displayName: 'repo',
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1,
|
||||
kind: 'git'
|
||||
}
|
||||
] as AppState['repos']
|
||||
const row = worktree()
|
||||
const worktreesByRepo = {
|
||||
'repo-1': [row]
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import { TerminalQuickCommandScopeField } from './TerminalQuickCommandScopeField
|
||||
|
||||
type TerminalQuickCommandAdvancedSectionProps = {
|
||||
draft: TerminalQuickCommand
|
||||
repos: Pick<Repo, 'id' | 'displayName' | 'path' | 'badgeColor'>[]
|
||||
repos: readonly Pick<Repo, 'id' | 'displayName' | 'path' | 'badgeColor'>[]
|
||||
advancedOpen: boolean
|
||||
selectedScope: ReturnType<typeof getTerminalQuickCommandScope>
|
||||
selectedRepoId: string
|
||||
|
||||
@@ -39,7 +39,7 @@ type TerminalQuickCommandDialogProps = {
|
||||
open: boolean
|
||||
mode: TerminalQuickCommandDialogMode
|
||||
command: TerminalQuickCommand
|
||||
repos?: Pick<Repo, 'id' | 'displayName' | 'path' | 'badgeColor'>[]
|
||||
repos?: readonly Pick<Repo, 'id' | 'displayName' | 'path' | 'badgeColor'>[]
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSave: (command: TerminalQuickCommand) => void
|
||||
}
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@ import { QUICK_COMMAND_TOGGLE_ITEM_CLASS } from './terminal-quick-command-toggle
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
type TerminalQuickCommandScopeFieldProps = {
|
||||
repos: Pick<Repo, 'id' | 'displayName' | 'path' | 'badgeColor'>[]
|
||||
repos: readonly Pick<Repo, 'id' | 'displayName' | 'path' | 'badgeColor'>[]
|
||||
selectedScope: TerminalQuickCommandScope
|
||||
selectedRepoId: string
|
||||
selectedRepoMissing: boolean
|
||||
@@ -32,7 +32,7 @@ function getRepoLabel(repo: Pick<Repo, 'displayName' | 'path'>): string {
|
||||
}
|
||||
|
||||
export function getQuickCommandProjectScopeRepoId(
|
||||
repos: Pick<Repo, 'id'>[],
|
||||
repos: readonly Pick<Repo, 'id'>[],
|
||||
lastRepoScopeId: string | null
|
||||
): string | null {
|
||||
return lastRepoScopeId ?? repos[0]?.id ?? null
|
||||
|
||||
@@ -16,7 +16,7 @@ import RepoBadgeLabel from '@/components/repo/RepoBadgeLabel'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
type RepoMultiComboboxProps = {
|
||||
repos: Repo[]
|
||||
repos: readonly Repo[]
|
||||
/** Currently selected repo ids. The component enforces `selected.size >= 1`
|
||||
* by disabling the last-selected checkbox. */
|
||||
selected: ReadonlySet<string>
|
||||
@@ -32,7 +32,10 @@ type RepoMultiComboboxProps = {
|
||||
triggerClassName?: string
|
||||
}
|
||||
|
||||
function renderTriggerLabel(repos: Repo[], selected: ReadonlySet<string>): React.JSX.Element {
|
||||
function renderTriggerLabel(
|
||||
repos: readonly Repo[],
|
||||
selected: ReadonlySet<string>
|
||||
): React.JSX.Element {
|
||||
if (repos.length === 0) {
|
||||
return (
|
||||
<span className="text-muted-foreground">
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { Repo } from '../../../shared/types'
|
||||
import { folderWorkspaceKey } from '../../../shared/workspace-scope'
|
||||
import { getAgentDetectionTargetKeyForWorktree } from './useAgentDetectionTarget'
|
||||
|
||||
describe('getAgentDetectionTargetKeyForWorktree', () => {
|
||||
it('uses an explicit runtime owner without scanning ambiguous child SSH repos', () => {
|
||||
let projectGroupReads = 0
|
||||
const repos = Array.from({ length: 100 }, (_, index) => {
|
||||
const repo = {
|
||||
const repos: readonly Repo[] = Array.from({ length: 100 }, (_, index) => {
|
||||
const repo: Repo = {
|
||||
id: `repo-${index}`,
|
||||
connectionId: `ssh-${index}`,
|
||||
executionHostId: `ssh:ssh-${index}`,
|
||||
path: `/workspace/repo-${index}`
|
||||
path: `/workspace/repo-${index}`,
|
||||
displayName: `repo-${index}`,
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1
|
||||
}
|
||||
Object.defineProperty(repo, 'projectGroupId', {
|
||||
enumerable: true,
|
||||
|
||||
@@ -291,7 +291,10 @@ describe('ensureHooksConfirmed', () => {
|
||||
repos: [
|
||||
{
|
||||
id: 'repo-1',
|
||||
path: '/repo-1',
|
||||
displayName: 'Repo One',
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1,
|
||||
hookSettings: {
|
||||
mode: 'auto',
|
||||
commandSourcePolicy: 'local-only',
|
||||
@@ -318,7 +321,10 @@ describe('ensureHooksConfirmed', () => {
|
||||
repos: [
|
||||
{
|
||||
id: 'repo-1',
|
||||
path: '/repo-1',
|
||||
displayName: 'Repo One',
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1,
|
||||
hookSettings: {
|
||||
mode: 'auto',
|
||||
scripts: { setup: 'echo local', archive: '' }
|
||||
|
||||
@@ -6,7 +6,7 @@ import { parseExecutionHostId } from '../../../shared/execution-host'
|
||||
export type FolderWorkspaceConnectionState = {
|
||||
folderWorkspaces: FolderWorkspace[]
|
||||
projectGroups: ProjectGroup[]
|
||||
repos: Repo[]
|
||||
repos: readonly Repo[]
|
||||
}
|
||||
|
||||
function getFolderScopeCandidateRepos(args: {
|
||||
|
||||
@@ -35,7 +35,7 @@ type StoreAccessor = () => {
|
||||
> | null
|
||||
setActiveWorktree: (worktreeId: string) => void
|
||||
createBrowserTab: (worktreeId: string, url: string, opts: { activate: boolean }) => unknown
|
||||
repos?: LocalhostLinkRepo[]
|
||||
repos?: readonly LocalhostLinkRepo[]
|
||||
projects?: LocalhostLinkProject[]
|
||||
worktreesByRepo?: Record<string, LocalhostLinkWorktree[]>
|
||||
allWorktrees?: () => LocalhostLinkWorktree[]
|
||||
|
||||
@@ -16,11 +16,15 @@ afterEach(() => {
|
||||
resetLocalPreflightContextCachesForTests()
|
||||
})
|
||||
|
||||
function makeRepos(id: string, path: string): AppState['repos'] {
|
||||
return [{ id, path, displayName: id, badgeColor: 'blue', addedAt: 1 }]
|
||||
}
|
||||
|
||||
function makeWslState(distro: string): AppState {
|
||||
return {
|
||||
activeRepoId: 'repo-1',
|
||||
activeWorktreeId: null,
|
||||
repos: [{ id: 'repo-1', path: `\\\\wsl.localhost\\${distro}\\home\\alice\\repo` }],
|
||||
repos: makeRepos('repo-1', `\\\\wsl.localhost\\${distro}\\home\\alice\\repo`),
|
||||
worktreesByRepo: {}
|
||||
} as AppState
|
||||
}
|
||||
@@ -29,7 +33,7 @@ function makeWindowsProjectState(projectId: string): AppState {
|
||||
return {
|
||||
activeRepoId: projectId,
|
||||
activeWorktreeId: null,
|
||||
repos: [{ id: projectId, path: `C:\\Users\\alice\\${projectId}` }],
|
||||
repos: makeRepos(projectId, `C:\\Users\\alice\\${projectId}`),
|
||||
settings: {},
|
||||
worktreesByRepo: {}
|
||||
} as AppState
|
||||
|
||||
@@ -11,26 +11,30 @@ import {
|
||||
} from './local-preflight-context'
|
||||
|
||||
function makeState(args: {
|
||||
repoPath?: string | null
|
||||
repoPath?: string
|
||||
worktreePath?: string | null
|
||||
repo?: Partial<Repo>
|
||||
worktree?: Partial<Worktree>
|
||||
}): AppState {
|
||||
const repoId = 'repo-1'
|
||||
const worktreeId = `${repoId}::worktree-1`
|
||||
const repos: AppState['repos'] =
|
||||
args.repoPath === undefined
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: repoId,
|
||||
path: args.repoPath,
|
||||
displayName: repoId,
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1,
|
||||
...args.repo
|
||||
}
|
||||
]
|
||||
return {
|
||||
activeRepoId: repoId,
|
||||
activeWorktreeId: args.worktreePath === undefined ? null : worktreeId,
|
||||
repos:
|
||||
args.repoPath === undefined
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: repoId,
|
||||
path: args.repoPath,
|
||||
...args.repo
|
||||
}
|
||||
],
|
||||
repos,
|
||||
worktreesByRepo:
|
||||
args.worktreePath === undefined
|
||||
? {}
|
||||
|
||||
@@ -38,7 +38,7 @@ function matchScore(repo: Repo, query: string): number | null {
|
||||
return null
|
||||
}
|
||||
|
||||
export function searchRepos(repos: Repo[], rawQuery: string): Repo[] {
|
||||
export function searchRepos(repos: readonly Repo[], rawQuery: string): readonly Repo[] {
|
||||
if (isRepoSearchQueryTooLarge(rawQuery)) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ async function resolveRepoSlug(
|
||||
}
|
||||
|
||||
async function buildIndex(
|
||||
repos: Repo[],
|
||||
repos: readonly Repo[],
|
||||
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined
|
||||
): Promise<{ index: SlugIndex; upstreamIndex: SlugIndex; retryDelayMs: number | null }> {
|
||||
// Why: evict cached entries for repos that no longer exist in state so
|
||||
|
||||
@@ -9,8 +9,8 @@ import { localhostWorktreeLabelRouteForPort } from './workspace-port-localhost-l
|
||||
// this is the single source for both reactive and imperative call sites.
|
||||
type LocalhostLabelLookupState = {
|
||||
settings?: Pick<GlobalSettings, 'localhostWorktreeLabelsEnabled'> | null
|
||||
repos?: Repo[]
|
||||
projects?: Project[]
|
||||
repos?: readonly Repo[]
|
||||
projects?: readonly Project[]
|
||||
getKnownWorktreeById?: (worktreeId: string) => { projectId?: string | null } | null | undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -327,6 +327,10 @@ describe('createDetectedAgentsSlice WSL context', () => {
|
||||
projects: [
|
||||
{
|
||||
id: 'repo-1',
|
||||
displayName: 'repo-1',
|
||||
badgeColor: 'blue',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
sourceRepoIds: ['repo-1'],
|
||||
localWindowsRuntimePreference: { kind: 'windows-host' }
|
||||
}
|
||||
|
||||
@@ -6408,17 +6408,18 @@ describe('createGitHubSlice.fetchWorkItems source/error envelope', () => {
|
||||
_meta: { runtimeId: 'remote-runtime' }
|
||||
})
|
||||
const store = createTestStore()
|
||||
const repos: AppState['repos'] = [
|
||||
{
|
||||
id: 'runtime-repo-id',
|
||||
path: '/server/repo',
|
||||
displayName: 'repo',
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1
|
||||
}
|
||||
]
|
||||
store.setState({
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' },
|
||||
repos: [
|
||||
{
|
||||
id: 'runtime-repo-id',
|
||||
path: '/server/repo',
|
||||
displayName: 'repo',
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1
|
||||
}
|
||||
]
|
||||
repos
|
||||
} as Partial<AppState>)
|
||||
|
||||
await store.getState().fetchWorkItems('caller-repo-id', '/server/repo', 24, 'is:open', {
|
||||
@@ -6508,17 +6509,18 @@ describe('createGitHubSlice.fetchWorkItems source/error envelope', () => {
|
||||
_meta: { runtimeId: 'source-runtime' }
|
||||
})
|
||||
const store = createTestStore()
|
||||
const repos: AppState['repos'] = [
|
||||
{
|
||||
id: 'local-repo-id',
|
||||
path: '/server/repo',
|
||||
displayName: 'repo',
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1
|
||||
}
|
||||
]
|
||||
store.setState({
|
||||
settings: { activeRuntimeEnvironmentId: 'focused-runtime' },
|
||||
repos: [
|
||||
{
|
||||
id: 'local-repo-id',
|
||||
path: '/server/repo',
|
||||
displayName: 'repo',
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1
|
||||
}
|
||||
]
|
||||
repos
|
||||
} as Partial<AppState>)
|
||||
|
||||
const sourceContext = {
|
||||
|
||||
@@ -265,11 +265,12 @@ describe('createPreflightSlice', () => {
|
||||
new Error('Project runtime requires repair before preflight: wsl-distro-required')
|
||||
)
|
||||
const store = createTestStore()
|
||||
const repos: AppState['repos'] = [makeRepo({ id: 'repo-1', path: 'C:\\repo' })]
|
||||
store.setState({
|
||||
settings: {
|
||||
localWindowsRuntimeDefault: { kind: 'wsl', distro: null }
|
||||
},
|
||||
repos: [makeRepo({ id: 'repo-1', path: 'C:\\repo' })],
|
||||
repos,
|
||||
worktreesByRepo: {},
|
||||
activeRepoId: 'repo-1',
|
||||
activeWorktreeId: null
|
||||
|
||||
@@ -27,7 +27,10 @@ function areReposEqual(a: Repo, b: Repo): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
export function reconcileFetchedRepos(previous: readonly Repo[], next: Repo[]): Repo[] {
|
||||
export function reconcileFetchedRepos(
|
||||
previous: readonly Repo[],
|
||||
next: readonly Repo[]
|
||||
): readonly Repo[] {
|
||||
const previousById = new Map(previous.map((repo) => [getRepoHostIdentity(repo), repo]))
|
||||
let identical = next.length === previous.length
|
||||
const reconciled = next.map((repo, index) => {
|
||||
@@ -41,5 +44,5 @@ export function reconcileFetchedRepos(previous: readonly Repo[], next: Repo[]):
|
||||
identical = false
|
||||
return repo
|
||||
})
|
||||
return identical ? (previous as Repo[]) : reconciled
|
||||
return identical ? previous : reconciled
|
||||
}
|
||||
|
||||
@@ -880,9 +880,9 @@ function mergeByIdentity<T>(
|
||||
|
||||
function mergeFetchedReposForHost(
|
||||
previous: readonly Repo[],
|
||||
fetched: Repo[],
|
||||
fetched: readonly Repo[],
|
||||
hostId: string
|
||||
): Repo[] {
|
||||
): readonly Repo[] {
|
||||
const fetchedWithProjectGroups = applyInheritedProjectGroups(previous, fetched)
|
||||
const fetchedIdentities = new Set(fetchedWithProjectGroups.map(getRepoHostIdentity))
|
||||
const preserved = previous.filter((repo) => {
|
||||
@@ -1049,7 +1049,7 @@ function mergeFetchedFolderWorkspacesForHost({
|
||||
}
|
||||
|
||||
type FetchedRepoCatalog = {
|
||||
repos: Repo[]
|
||||
repos: readonly Repo[]
|
||||
projectHostSetupCompatibility: ProjectHostSetupProjection
|
||||
hostId: ReturnType<typeof getRuntimeTargetHostId>
|
||||
}
|
||||
@@ -1110,7 +1110,7 @@ function mergeFetchedRepoCatalog(
|
||||
catalog: FetchedRepoCatalog,
|
||||
currentRepos: readonly Repo[]
|
||||
): {
|
||||
repos: Repo[]
|
||||
repos: readonly Repo[]
|
||||
projectHostSetupCompatibility: ProjectHostSetupProjection
|
||||
hostId: ReturnType<typeof getRuntimeTargetHostId>
|
||||
} {
|
||||
@@ -1619,7 +1619,7 @@ function getFolderWorkspacePathStatusRequestSnapshotForRead(
|
||||
}
|
||||
|
||||
export type RepoSlice = {
|
||||
repos: Repo[]
|
||||
repos: readonly Repo[]
|
||||
projects: Project[]
|
||||
projectHostSetups: ProjectHostSetup[]
|
||||
projectGroups: ProjectGroup[]
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Repo } from '../../../../shared/types'
|
||||
import { getRepoExecutionHostId, toSshExecutionHostId } from '../../../../shared/execution-host'
|
||||
|
||||
export type SshRepoReconciliation = {
|
||||
repos: Repo[]
|
||||
repos: readonly Repo[]
|
||||
pendingReadoptions: SshRepoReadoption[]
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export function reconcileReadoptedSshRepoRows(
|
||||
// Why: pruning nothing must hand back the input array, or the copy alone would defeat the
|
||||
// referential stability reconcileFetchedRepos just established upstream.
|
||||
if (prunedOwners.size === 0) {
|
||||
return { repos: repos as Repo[], pendingReadoptions }
|
||||
return { repos, pendingReadoptions }
|
||||
}
|
||||
return {
|
||||
repos: repos.filter(
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
WorktreeLineage,
|
||||
WorkspaceLineage,
|
||||
ProjectHostSetup,
|
||||
Repo,
|
||||
WorktreeMeta
|
||||
} from '../../../../shared/types'
|
||||
import type { RuntimeWorktreeListResult } from '../../../../shared/runtime-types'
|
||||
@@ -6065,7 +6066,7 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
|
||||
const repoIdsWithRemovedOwners = new Set<string>()
|
||||
const survivingRepoIds = new Set<string>()
|
||||
const repoIdsWithSurvivingOwners = new Set<string>()
|
||||
const survivingRepos: AppState['repos'] = []
|
||||
const survivingRepos: Repo[] = []
|
||||
for (const repo of s.repos) {
|
||||
if (isRemovedRuntimeHostId(getRepoExecutionHostId(repo), removed)) {
|
||||
repoIdsWithRemovedOwners.add(repo.id)
|
||||
|
||||
@@ -41,12 +41,12 @@ export function getManualRepoOrder(repos: readonly Repo[]): ManualRepoOrderEntry
|
||||
export function applyManualRepoOrder(
|
||||
repos: readonly Repo[],
|
||||
order: readonly ManualRepoOrderEntry[] | null | undefined
|
||||
): Repo[] {
|
||||
): readonly Repo[] {
|
||||
const normalized = normalizeManualRepoOrder(order)
|
||||
// Why: results flow straight back into `repos`, so a reorder that moves nothing must return
|
||||
// the input — a fresh array would invalidate every identity-keyed repo memo downstream.
|
||||
if (normalized.length === 0) {
|
||||
return repos as Repo[]
|
||||
return repos
|
||||
}
|
||||
const rankByKey = new Map(normalized.map((entry, index) => [getEntryKey(entry), index]))
|
||||
const ordered = repos
|
||||
@@ -68,5 +68,5 @@ export function applyManualRepoOrder(
|
||||
return a.rank - b.rank || a.index - b.index
|
||||
})
|
||||
.map(({ repo }) => repo)
|
||||
return ordered.every((repo, index) => repo === repos[index]) ? (repos as Repo[]) : ordered
|
||||
return ordered.every((repo, index) => repo === repos[index]) ? repos : ordered
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user