mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
refactor(renderer): read workspace records through typed selectors (#31462)
* refactor(renderer): read workspace records through typed selectors * refactor(renderer): keep only the selectors with readers; type the host owner lookup
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import process from 'node:process'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { readTrackedSources, runCountRatchet } from './ratchet-baseline.mjs'
|
||||
|
||||
// Ratchet on renderer reads of the workspace record fields outside their selector module. The
|
||||
// sidebar's records move to main's feed (sidebar design §2, §9), so a reader that touches the
|
||||
// store field directly would silently keep reading the old copy. Counts may only shrink.
|
||||
// A lower bound: destructuring and computed access are not seen; the compiler is the second
|
||||
// guard once the fields leave AppState. Writers stay in the baseline
|
||||
// until their cut moves them.
|
||||
|
||||
const BASELINE_PATH = 'config/workspace-record-read-baseline.txt'
|
||||
const SCAN_ROOTS = ['src/renderer/src/']
|
||||
// `x.worktreesByRepo`, `x?.folderWorkspaces`; not `...worktreesByRepo` or `window.api.folderWorkspaces`.
|
||||
const RAW_READ =
|
||||
/(?<!\.\.)(?<!\bapi\??)\.(?:worktreesByRepo|folderWorkspaces|detectedWorktreesByRepo)\b/g
|
||||
|
||||
/** Files allowed to read the fields directly, with the reason. Keep each reason true. */
|
||||
export const SELECTOR_FILES = new Map([
|
||||
[
|
||||
'src/renderer/src/store/workspace-record-selectors.ts',
|
||||
'the selector module is the one read path by definition'
|
||||
]
|
||||
])
|
||||
|
||||
export function isScannedFile(rel) {
|
||||
return (
|
||||
/\.tsx?$/.test(rel) &&
|
||||
!rel.endsWith('.d.ts') &&
|
||||
!/\.(?:test|spec)\.tsx?$|fixture|test-harness|test-support/.test(rel) &&
|
||||
!SELECTOR_FILES.has(rel)
|
||||
)
|
||||
}
|
||||
|
||||
/** Drops comments and the contents of quoted strings, so prose and i18n keys never count. */
|
||||
export function stripCommentsAndQuotedStrings(text) {
|
||||
let out = ''
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i]
|
||||
if (char === "'" || char === '"') {
|
||||
let j = i + 1
|
||||
while (j < text.length && text[j] !== char && text[j] !== '\n') {
|
||||
j += text[j] === '\\' ? 2 : 1
|
||||
}
|
||||
out += `${char}${char}`
|
||||
i = j
|
||||
} else if (char === '/' && text[i + 1] === '/') {
|
||||
const end = text.indexOf('\n', i)
|
||||
i = end === -1 ? text.length : end - 1
|
||||
} else if (char === '/' && text[i + 1] === '*') {
|
||||
const end = text.indexOf('*/', i + 2)
|
||||
i = end === -1 ? text.length : end + 1
|
||||
out += ' '
|
||||
} else {
|
||||
out += char
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function countWorkspaceRecordReads(text) {
|
||||
return [...stripCommentsAndQuotedStrings(text).matchAll(RAW_READ)].length
|
||||
}
|
||||
|
||||
export function collectWorkspaceRecordReads(root = process.cwd()) {
|
||||
const counts = new Map()
|
||||
for (const [rel, text] of readTrackedSources(root, SCAN_ROOTS, isScannedFile)) {
|
||||
const count = countWorkspaceRecordReads(text)
|
||||
if (count > 0) {
|
||||
counts.set(rel, count)
|
||||
}
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
export function main(root = process.cwd(), arg = undefined) {
|
||||
return runCountRatchet(
|
||||
{
|
||||
name: 'workspace-record-read',
|
||||
baselinePath: BASELINE_PATH,
|
||||
unit: 'direct workspace record field read(s)',
|
||||
header: [
|
||||
'# Renderer reads of `worktreesByRepo` / `folderWorkspaces` / `detectedWorktreesByRepo`',
|
||||
'# outside src/renderer/src/store/workspace-record-selectors.ts, as `<count> <file> # <why>`.',
|
||||
'# This is a RATCHET: counts may only go DOWN. Read through the selectors instead.',
|
||||
'# `P5b-2 must clear` rows read store state through a loosely typed or optional field: once the',
|
||||
'# fields leave the store they read undefined and silently fall back, so P5b-2 must move them.',
|
||||
'# Prune after a fix: pnpm check:workspace-record-read-ratchet --prune'
|
||||
],
|
||||
collect: collectWorkspaceRecordReads,
|
||||
fix: [
|
||||
'Read workspace records through src/renderer/src/store/workspace-record-selectors.ts:',
|
||||
' selectWorkspaceByHostTarget(state, target) the owner of a HostWorktreeTarget',
|
||||
' selectWorktreesByRepo / selectFolderWorkspaces / selectDetectedWorktreesByRepo',
|
||||
' compatibility views of the old fields'
|
||||
]
|
||||
},
|
||||
root,
|
||||
arg
|
||||
)
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
process.exit(main(process.cwd(), process.argv[2]))
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
countWorkspaceRecordReads,
|
||||
isScannedFile,
|
||||
SELECTOR_FILES
|
||||
} from './check-workspace-record-read-ratchet.mjs'
|
||||
|
||||
describe('countWorkspaceRecordReads', () => {
|
||||
it('counts direct reads of the record fields', () => {
|
||||
const text = [
|
||||
'const rows = state.worktreesByRepo[repoId]',
|
||||
'useAppStore((s) => s.folderWorkspaces)',
|
||||
'const detected = get().detectedWorktreesByRepo',
|
||||
'const maybe = input?.worktreesByRepo ?? {}'
|
||||
].join('\n')
|
||||
expect(countWorkspaceRecordReads(text)).toBe(4)
|
||||
})
|
||||
|
||||
it('ignores the preload API, spreads, comments, strings and selector calls', () => {
|
||||
const text = [
|
||||
'await window.api.folderWorkspaces.update(args)',
|
||||
'const next = { ...worktreesByRepo, [repoId]: rows }',
|
||||
'// state.worktreesByRepo used to be read here',
|
||||
"t('sidebar.index.folderWorkspaces')",
|
||||
"type T = AppState['detectedWorktreesByRepo']",
|
||||
'const rows = selectWorktreesByRepo(state)'
|
||||
].join('\n')
|
||||
expect(countWorkspaceRecordReads(text)).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isScannedFile', () => {
|
||||
it('scans renderer sources but not tests or the selector module', () => {
|
||||
expect(isScannedFile('src/renderer/src/lib/foo.ts')).toBe(true)
|
||||
expect(isScannedFile('src/renderer/src/components/Foo.tsx')).toBe(true)
|
||||
expect(isScannedFile('src/renderer/src/lib/foo.test.ts')).toBe(false)
|
||||
expect(isScannedFile('src/renderer/src/lib/foo.test-fixture.ts')).toBe(false)
|
||||
for (const file of SELECTOR_FILES.keys()) {
|
||||
expect(isScannedFile(file)).toBe(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,84 @@
|
||||
# Renderer reads of `worktreesByRepo` / `folderWorkspaces` / `detectedWorktreesByRepo`
|
||||
# outside src/renderer/src/store/workspace-record-selectors.ts, as `<count> <file> # <why>`.
|
||||
# This is a RATCHET: counts may only go DOWN. Read through the selectors instead.
|
||||
# `P5b-2 must clear` rows read store state through a loosely typed or optional field: once the
|
||||
# fields leave the store they read undefined and silently fall back, so P5b-2 must move them.
|
||||
# Prune after a fix: pnpm check:workspace-record-read-ratchet --prune
|
||||
|
||||
3 src/renderer/src/attention/notification-subject-owner.ts # P5b-2 must clear: loosely typed state param
|
||||
4 src/renderer/src/components/dashboard/build-dashboard-bucket-counts.ts # P5b-2 must clear: optional state fields; memo compare goes stale when absent
|
||||
3 src/renderer/src/components/dashboard/dashboard-card-terminal-input.ts # P5b-2 must clear: optional state fields default to empty
|
||||
1 src/renderer/src/components/dashboard/dashboard-snapshot-workspaces.ts # P5b-2 must clear: optional state field defaults to empty
|
||||
1 src/renderer/src/components/dashboard/dashboard-worktree-launch-options.ts # P5b-2 must clear: optional state field defaults to empty
|
||||
4 src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts # P5b-2 must clear: change detection on optional fields never fires when absent
|
||||
2 src/renderer/src/components/dashboard/useRetainedAgents.ts # reads a passed-in copy
|
||||
6 src/renderer/src/components/editor/closed-editor-tab-controller.ts # P5b-2 must clear: identity compare of store slices passes when absent
|
||||
1 src/renderer/src/components/editor/markdown-document-worktree-path-selector.ts # P5b-2 must clear: optional state field
|
||||
1 src/renderer/src/components/feature-wall/feature-wall-setup-progress.ts # reads a passed-in copy
|
||||
3 src/renderer/src/components/quick-open-file-list.ts # P5b-2 must clear: store read; file at the max-lines limit
|
||||
2 src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts # P5b-2 must clear: optional state fields default to empty
|
||||
1 src/renderer/src/components/sidebar/add-repo-skip-finalization.ts # P5b-2 must clear: loosely typed state param
|
||||
1 src/renderer/src/components/sidebar/imported-worktrees-card-candidates.ts # reads a passed-in copy
|
||||
1 src/renderer/src/components/sidebar/new-external-worktrees-inbox-candidates.ts # reads a passed-in copy
|
||||
3 src/renderer/src/components/sidebar/worktree-list/listing/use-main-list-model.ts # reads a passed-in copy
|
||||
2 src/renderer/src/components/sidebar/worktree-list/listing/use-window-list-model.ts # reads a passed-in copy
|
||||
1 src/renderer/src/components/sidebar/worktree-list/navigation/pending-reveal-inputs.ts # reads a passed-in copy
|
||||
2 src/renderer/src/components/sidebar/worktree-list/navigation/use-pending-reveal.ts # reads a passed-in copy
|
||||
2 src/renderer/src/components/sidebar/worktree-list/viewport/VirtualizedWorktreeViewport.tsx # reads a passed-in copy
|
||||
1 src/renderer/src/components/sidebar/worktree-manual-order-catalog.ts # reads a passed-in copy
|
||||
2 src/renderer/src/components/skills/skill-install-workspace-choices.ts # reads a passed-in copy
|
||||
1 src/renderer/src/components/terminal-pane/pty-connection/agent-idle-working-handlers.ts # P5b-2 must clear: session store snapshot
|
||||
1 src/renderer/src/components/terminal-pane/terminal-worktree-path-link.ts # P5b-2 must clear: loosely typed state param
|
||||
1 src/renderer/src/components/workspace-space/space-window-state.ts # reads a passed-in copy
|
||||
1 src/renderer/src/hooks/composer-state/composer-source-state.ts # P5b-2 must clear: composer store snapshot
|
||||
2 src/renderer/src/hooks/composer-state/composer-target-state.ts # P5b-2 must clear: composer store snapshot
|
||||
5 src/renderer/src/lib/agent-background-session-test-state.ts # test state builder
|
||||
1 src/renderer/src/lib/connection-owner-resolution.ts # P5b-2 must clear: optional-chain compare of a previous slice
|
||||
2 src/renderer/src/lib/direct-ssh-target-scope.ts # P5b-2 must clear: optional input fields default to empty
|
||||
3 src/renderer/src/lib/duplicate-paired-servers.ts # P5b-2 must clear: optional state fields default to empty
|
||||
1 src/renderer/src/lib/folder-workspace-runtime-owner.ts # P5b-2 must clear: loosely typed state param
|
||||
1 src/renderer/src/lib/http-link-routing.ts # P5b-2 must clear: optional state field defaults to empty
|
||||
2 src/renderer/src/lib/local-project-runtime-workspace.ts # P5b-2 must clear: optional state field defaults to empty
|
||||
3 src/renderer/src/lib/resolve-owner.ts # P5b-2 must clear: optional state fields; a missing copy reads as no match
|
||||
3 src/renderer/src/lib/resolved-worktree-execution-host.ts # P5b-2 must clear: loosely typed state param
|
||||
2 src/renderer/src/lib/runtime-session-mirror-owners.ts # P5b-2 must clear: optional state fields default to empty
|
||||
3 src/renderer/src/lib/sleeping-record-execution-host-scope.ts # P5b-2 must clear: loosely typed state param
|
||||
4 src/renderer/src/lib/terminal-worktree-route.ts # P5b-2 must clear: fails open for spawn when the fields are undefined
|
||||
3 src/renderer/src/lib/typing-latency/diagnostic-summary.ts # P5b-2 must clear: optional state field
|
||||
2 src/renderer/src/lib/unread-badge-count.ts # reads a passed-in copy
|
||||
1 src/renderer/src/lib/workspace-directory.ts # P5b-2 must clear: optional state field defaults to empty
|
||||
3 src/renderer/src/lib/workspace-session-host-persistence.ts # P5b-2 must clear: loosely typed state param
|
||||
1 src/renderer/src/lib/workspace-session.ts # P5b-2 must clear: store snapshot read; file at the max-lines limit
|
||||
2 src/renderer/src/lib/workspace-terminal-host-authority.ts # P5b-2 must clear: optional state fields
|
||||
1 src/renderer/src/lib/worktree-activation-pty-inventory.ts # P5b-2 must clear: loosely typed state param
|
||||
1 src/renderer/src/lib/worktree-initial-terminal-seeding.ts # P5b-2 must clear: treats an undefined field as a narrow adapter
|
||||
2 src/renderer/src/lib/worktree-operation-catalog-route.ts # P5b-2 must clear: loosely typed state param
|
||||
6 src/renderer/src/lib/worktree-operation-route.ts # P5b-2 must clear: optional state fields default to empty
|
||||
2 src/renderer/src/lib/worktree-runtime-owner.ts # P5b-2 must clear: loosely typed state param
|
||||
3 src/renderer/src/runtime/local-structured-session-tab-retirement.ts # P5b-2 must clear: optional state fields default to empty
|
||||
2 src/renderer/src/runtime/sync-runtime-graph/sync-key.ts # reads a passed-in copy
|
||||
3 src/renderer/src/store/folder-workspaces/folder-workspace-catalog-actions.ts # store writer
|
||||
7 src/renderer/src/store/folder-workspaces/folder-workspace-catalog.ts # store writer
|
||||
5 src/renderer/src/store/folder-workspaces/folder-workspace-mutations.ts # store writer
|
||||
1 src/renderer/src/store/project-groups/project-group-removal-state.ts # store writer
|
||||
4 src/renderer/src/store/repos/converted-ssh-host-rows.ts # store writer
|
||||
5 src/renderer/src/store/repos/repo-catalog-merge.ts # store writer
|
||||
5 src/renderer/src/store/repos/repo-removal.ts # store writer
|
||||
6 src/renderer/src/store/slices/diff-comment-persistence.ts # store writer
|
||||
12 src/renderer/src/store/slices/workspace-attention/workspace-unread-flag.ts # store writer
|
||||
2 src/renderer/src/store/slices/worktrees/create/created-worktree-state-merge.ts # store writer
|
||||
1 src/renderer/src/store/slices/worktrees/listing/detected-worktree-meta.ts # P5b-2 must clear: feeds an existing row-type cast
|
||||
3 src/renderer/src/store/slices/worktrees/listing/fetch-detected-worktrees.ts # store writer
|
||||
13 src/renderer/src/store/slices/worktrees/listing/fetched-worktree-merge.ts # store writer
|
||||
6 src/renderer/src/store/slices/worktrees/listing/known-ssh-worktree-fetch.ts # store writer
|
||||
3 src/renderer/src/store/slices/worktrees/listing/worktree-sort-order-patch.ts # store writer
|
||||
6 src/renderer/src/store/slices/worktrees/metadata/hosted-review-link-mutation.ts # store writer
|
||||
6 src/renderer/src/store/slices/worktrees/metadata/update-worktree-meta.ts # store writer
|
||||
8 src/renderer/src/store/slices/worktrees/metadata/update-worktrees-meta.ts # store writer
|
||||
3 src/renderer/src/store/slices/worktrees/metadata/worktree-git-identity-update.ts # store writer
|
||||
1 src/renderer/src/store/slices/worktrees/metadata/worktree-lineage-refresh.ts # store writer
|
||||
6 src/renderer/src/store/slices/worktrees/session/worktree-activity-bump.ts # store writer
|
||||
6 src/renderer/src/store/slices/worktrees/teardown/host-qualified-worktree-row-removal.ts # store writer
|
||||
8 src/renderer/src/store/slices/worktrees/teardown/purge-stale-runtime-host-state.ts # store writer
|
||||
1 src/renderer/src/store/slices/worktrees/teardown/remove-worktree-store-cleanup.ts # store writer
|
||||
8 src/renderer/src/store/terminals/workspace-terminal-hydration.ts # store writer
|
||||
+2
-1
@@ -14,7 +14,7 @@
|
||||
"audit:perf": "oxlint --config config/oxlint-performance-audit.json --format json src",
|
||||
"test:perf:contracts": "vitest run --config config/vitest.performance.config.ts",
|
||||
"format": "oxfmt --write .",
|
||||
"lint": "oxlint && pnpm run audit:anti-slop && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:dead-classes && pnpm run check:max-lines-ratchet && pnpm run check:ts-nocheck-ratchet && pnpm run check:runtime-electron-ratchet && pnpm run check:focus-read-ratchet && pnpm run check:layout-writer-ratchets && pnpm run check:path-literal-ratchet && pnpm run check:legacy-relay-fence-ratchet && pnpm run check:unproven-pty-retirement-ratchet && pnpm run check:id-only-store-write-ratchet && pnpm run check:repo-host-read-ratchet && pnpm run check:host-list-render-ratchet && pnpm run check:readme-local-links && pnpm run check:node-runtime-pin && pnpm run check:mobile-shared-dependency-versions && pnpm run verify:rpc-params-catalog && pnpm run verify:acp-protocol && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalogs && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage && pnpm run check:process-host-imports && pnpm run check:main-sync-spawn-ratchet && pnpm run check:compat-boundary-imports",
|
||||
"lint": "oxlint && pnpm run audit:anti-slop && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:dead-classes && pnpm run check:max-lines-ratchet && pnpm run check:ts-nocheck-ratchet && pnpm run check:runtime-electron-ratchet && pnpm run check:focus-read-ratchet && pnpm run check:layout-writer-ratchets && pnpm run check:path-literal-ratchet && pnpm run check:legacy-relay-fence-ratchet && pnpm run check:unproven-pty-retirement-ratchet && pnpm run check:id-only-store-write-ratchet && pnpm run check:repo-host-read-ratchet && pnpm run check:workspace-record-read-ratchet && pnpm run check:host-list-render-ratchet && pnpm run check:readme-local-links && pnpm run check:node-runtime-pin && pnpm run check:mobile-shared-dependency-versions && pnpm run verify:rpc-params-catalog && pnpm run verify:acp-protocol && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalogs && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage && pnpm run check:process-host-imports && pnpm run check:main-sync-spawn-ratchet && pnpm run check:compat-boundary-imports",
|
||||
"audit:code-quality": "pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run audit:react-doctor",
|
||||
"audit:code-quality:native": "oxlint --config config/oxlint-code-quality-native-plugins.json src config tests mobile --deny-warnings",
|
||||
"audit:code-quality:type-aware": "oxlint --type-aware --config config/oxlint-code-quality-type-aware.json src config tests --deny-warnings",
|
||||
@@ -45,6 +45,7 @@
|
||||
"check:unproven-pty-retirement-ratchet": "node config/scripts/check-unproven-pty-retirement-ratchet.mjs",
|
||||
"check:id-only-store-write-ratchet": "node config/scripts/check-id-only-store-write-ratchet.mjs",
|
||||
"check:repo-host-read-ratchet": "node config/scripts/check-repo-host-read-ratchet.mjs",
|
||||
"check:workspace-record-read-ratchet": "node config/scripts/check-workspace-record-read-ratchet.mjs",
|
||||
"check:worktree-id-composer-ratchet": "node config/scripts/check-worktree-id-composer-ratchet.mjs",
|
||||
"check:e2e-typecheck-ratchet": "node config/scripts/check-e2e-typecheck-ratchet.mjs",
|
||||
"check:runtime-electron-ratchet": "node config/scripts/check-runtime-electron-ratchet.mjs",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
reviewRefreshIntervalMs,
|
||||
REVIEW_REFRESH_COOLDOWN_MS
|
||||
} from '../../../shared/review-refresh-policy'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export function visibleReviewWorktreeIdsForState(state: AppState): string[] {
|
||||
const ids = new Set(state.visibleReviewCardWorktreeIds ?? [])
|
||||
@@ -109,7 +110,7 @@ export function createVisibleReviewReportIdentitySelector(): (state: AppState) =
|
||||
rightSidebarTab: state.rightSidebarTab,
|
||||
visibleReviewCardWorktreeIds: state.visibleReviewCardWorktreeIds,
|
||||
repos: state.repos,
|
||||
worktreesByRepo: state.worktreesByRepo,
|
||||
worktreesByRepo: selectWorktreesByRepo(state),
|
||||
settings: state.settings,
|
||||
sshConnectionStates: state.sshConnectionStates,
|
||||
sshConnectedGeneration: state.sshConnectedGeneration,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { sidebarHasActiveFilters } from '@/components/sidebar/sidebar-filter-act
|
||||
import { computeVisibleWorktreeIds } from '@/components/sidebar/compute-visible-worktrees'
|
||||
import { buildVisibleWorktreeOptionsFromState } from '@/components/sidebar/visible-worktree-options-from-state'
|
||||
import { isFolderWorkspaceFromOtherDevice } from '../../../shared/workspace-creator-visibility'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
/**
|
||||
* Whether attention from this workspace must raise nothing — no unread, banner, sound or push —
|
||||
@@ -32,14 +33,14 @@ export function isWorkspaceNotificationMutedBySidebarFilters(
|
||||
const inputs = buildVisibleWorktreeOptionsFromState(state, repoMap)
|
||||
const scope = parseWorkspaceKey(workspaceId)
|
||||
if (scope?.type === 'folder') {
|
||||
const folder = state.folderWorkspaces.find((item) => item.id === scope.folderWorkspaceId)
|
||||
const folder = selectFolderWorkspaces(state).find((item) => item.id === scope.folderWorkspaceId)
|
||||
return (
|
||||
folder !== undefined &&
|
||||
inputs.hideWorkspacesFromOtherDevices &&
|
||||
isFolderWorkspaceFromOtherDevice(folder, inputs.pairedDeviceIdsByEnvironment)
|
||||
)
|
||||
}
|
||||
const known = getIndexedAllWorktrees(state.worktreesByRepo).some(
|
||||
const known = getIndexedAllWorktrees(selectWorktreesByRepo(state)).some(
|
||||
(worktree) => worktree.id === workspaceId && !worktree.isArchived
|
||||
)
|
||||
if (!known) {
|
||||
@@ -47,7 +48,7 @@ export function isWorkspaceNotificationMutedBySidebarFilters(
|
||||
}
|
||||
// Why the full sidebar pass per call: measured at ~0.2-0.3 ms with 1,000 workspaces, and a
|
||||
// per-row shortcut would have to mirror every way the sidebar adds rows back.
|
||||
const visibleIds = computeVisibleWorktreeIds(state.worktreesByRepo, [], {
|
||||
const visibleIds = computeVisibleWorktreeIds(selectWorktreesByRepo(state), [], {
|
||||
...inputs,
|
||||
filterRepoIds: [],
|
||||
workspaceHostScope: ALL_EXECUTION_HOSTS_SCOPE,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useLinearIssueWorkspaceDetail } from './linear-issue-workspace-detail-s
|
||||
import { LinearIssueWorkspaceHeader } from './linear-issue-workspace-header'
|
||||
import { LinearIssueWorkspaceSidebar } from './linear-issue-workspace-sidebar'
|
||||
import { defaultScopeSource } from '@/lib/default-creation-host'
|
||||
import { selectFolderWorkspaces } from '@/store/workspace-record-selectors'
|
||||
|
||||
type LinearIssueWorkspaceProps = {
|
||||
issue: LinearIssue | null
|
||||
@@ -52,7 +53,7 @@ function LinearIssueWorkspaceIssue({
|
||||
const settings = useAppStore((state) => state.settings)
|
||||
const providerSettings = sourceContext ?? defaultScopeSource(settings)
|
||||
const allWorktrees = useAllWorktrees()
|
||||
const folderWorkspaces = useAppStore((state) => state.folderWorkspaces)
|
||||
const folderWorkspaces = useAppStore((state) => selectFolderWorkspaces(state))
|
||||
const attachmentWorkspaces = useMemo(
|
||||
() => [...allWorktrees, ...folderWorkspaces.map(folderWorkspaceToWorktree)],
|
||||
[allWorktrees, folderWorkspaces]
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type LinearLocalComment
|
||||
} from '@/components/linear-item-drawer-types'
|
||||
import { renderLinearItemDrawerSheet } from '@/components/linear-item-drawer-sheet'
|
||||
import { selectFolderWorkspaces } from '@/store/workspace-record-selectors'
|
||||
import { defaultScopeSource } from '@/lib/default-creation-host'
|
||||
|
||||
export { LinearIssueEditSection } from '@/components/linear-item-drawer-edit-section'
|
||||
@@ -40,7 +41,7 @@ export default function LinearItemDrawer({
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const providerSettings = sourceContext ?? defaultScopeSource(settings)
|
||||
const allWorktrees = useAllWorktrees()
|
||||
const folderWorkspaces = useAppStore((s) => s.folderWorkspaces)
|
||||
const folderWorkspaces = useAppStore((s) => selectFolderWorkspaces(s))
|
||||
const attachmentWorkspaces = useMemo(
|
||||
() => [...allWorktrees, ...folderWorkspaces.map(folderWorkspaceToWorktree)],
|
||||
[allWorktrees, folderWorkspaces]
|
||||
|
||||
@@ -22,6 +22,11 @@ import {
|
||||
} from './activity-terminal-portal-status'
|
||||
import type { ActivityTerminalPortalSlotId } from './activity-thread-types'
|
||||
import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router'
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectFolderWorkspaces,
|
||||
selectWorktreesByRepo
|
||||
} from '@/store/workspace-record-selectors'
|
||||
|
||||
export * from './activity-prototype-page-exports'
|
||||
|
||||
@@ -260,19 +265,23 @@ export default function ActivityPrototypePage(): React.JSX.Element {
|
||||
setSelectedPaneKey
|
||||
})
|
||||
|
||||
const worktreesByRepo = selectWorktreesByRepo(storeData)
|
||||
const detectedWorktreesByRepo = selectDetectedWorktreesByRepo(storeData)
|
||||
const folderWorkspaces = selectFolderWorkspaces(storeData)
|
||||
|
||||
const canJumpToWorkspace = useCallback(
|
||||
(thread: Parameters<typeof hasActivityThreadWorkspace>[0]) =>
|
||||
hasActivityThreadWorkspace(thread, {
|
||||
worktreesByRepo: storeData.worktreesByRepo,
|
||||
detectedWorktreesByRepo: storeData.detectedWorktreesByRepo,
|
||||
folderWorkspaces: storeData.folderWorkspaces,
|
||||
worktreesByRepo,
|
||||
detectedWorktreesByRepo,
|
||||
folderWorkspaces,
|
||||
floatingWorkspacePath: storeData.floatingWorkspacePath,
|
||||
defaultHostId: storeData.defaultHostId
|
||||
}),
|
||||
[
|
||||
storeData.worktreesByRepo,
|
||||
storeData.detectedWorktreesByRepo,
|
||||
storeData.folderWorkspaces,
|
||||
worktreesByRepo,
|
||||
detectedWorktreesByRepo,
|
||||
folderWorkspaces,
|
||||
storeData.floatingWorkspacePath,
|
||||
storeData.defaultHostId
|
||||
]
|
||||
|
||||
@@ -23,6 +23,11 @@ import {
|
||||
buildActivityThreadGroups,
|
||||
isActivitySearchQueryTooLarge
|
||||
} from './activity-thread-grouping'
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectFolderWorkspaces,
|
||||
selectWorktreesByRepo
|
||||
} from '@/store/workspace-record-selectors'
|
||||
import type {
|
||||
ActivityGroupBy,
|
||||
ActivityThreadGroup,
|
||||
@@ -124,10 +129,10 @@ export function useAgentPaneThreads(args: {
|
||||
return projected
|
||||
})(),
|
||||
repos: s.repos,
|
||||
worktreesByRepo: s.worktreesByRepo,
|
||||
folderWorkspaces: s.folderWorkspaces,
|
||||
worktreesByRepo: selectWorktreesByRepo(s),
|
||||
folderWorkspaces: selectFolderWorkspaces(s),
|
||||
floatingWorkspacePath: s.floatingWorkspacePath,
|
||||
detectedWorktreesByRepo: s.detectedWorktreesByRepo,
|
||||
detectedWorktreesByRepo: selectDetectedWorktreesByRepo(s),
|
||||
getKnownWorktreeById: s.getKnownWorktreeById,
|
||||
worktreeMap: getWorktreeMapFromState(s),
|
||||
repoMap: getRepoMapFromState(s),
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { AutomationsPageDestinationState } from './use-automations-page-des
|
||||
import type { AutomationsPageDestinationFormState } from './use-automations-page-destination-form'
|
||||
import type { AutomationsPageLocalState } from './use-automations-page-local-state'
|
||||
import type { AutomationsPageStoreState } from './use-automations-page-store-state'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
/** Dialog open/edit actions, including host-captured re-reads and project changes. */
|
||||
export function useAutomationEditorActions({
|
||||
@@ -152,7 +153,7 @@ export function useAutomationEditorActions({
|
||||
baseBranch: ''
|
||||
}))
|
||||
void fetchWorktrees(projectId, worktreeFetchOptions).then(() => {
|
||||
const latestWorktrees = useAppStore.getState().worktreesByRepo[projectId] ?? []
|
||||
const latestWorktrees = selectWorktreesByRepo(useAppStore.getState())[projectId] ?? []
|
||||
const latestWorktree = getDefaultWorktree(latestWorktrees)
|
||||
if (!latestWorktree) {
|
||||
return
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
automationWorktreeForRow,
|
||||
type AutomationListRow
|
||||
} from './automation-list-row-identity'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
const AGENTS = getAgentCatalog().map((agent) => agent.id)
|
||||
|
||||
@@ -18,8 +19,8 @@ const AGENTS = getAgentCatalog().map((agent) => agent.id)
|
||||
export function useAutomationsPageStoreState() {
|
||||
const repos = useAppStore((s) => s.repos)
|
||||
const projectHostSetups = useAppStore((s) => s.projectHostSetups)
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const folderWorkspaces = useAppStore((s) => s.folderWorkspaces)
|
||||
const worktreesByRepo = useAppStore((s) => selectWorktreesByRepo(s))
|
||||
const folderWorkspaces = useAppStore((s) => selectFolderWorkspaces(s))
|
||||
const unifiedTabsByWorktree = useAppStore((s) => s.unifiedTabsByWorktree)
|
||||
const terminalLayoutsByTabId = useAppStore((s) => s.terminalLayoutsByTabId)
|
||||
const ptyIdsByTabId = useAppStore((s) => s.ptyIdsByTabId)
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
runActiveNativeChatSplit
|
||||
} from '@/components/native-chat/native-chat-layout-actions'
|
||||
import type { NativeChatSplitDirection } from '@/components/native-chat/native-chat-split-shortcut'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export type CmdJUnavailableReason =
|
||||
| 'loading'
|
||||
@@ -160,7 +161,7 @@ export function buildCmdJQuickActionContext(args: {
|
||||
}): CmdJQuickActionContext {
|
||||
const activeWorktreeId = args.state.activeWorktreeId
|
||||
const activeWorktree = activeWorktreeId
|
||||
? (findWorktreeById(args.state.worktreesByRepo, activeWorktreeId) ?? null)
|
||||
? (findWorktreeById(selectWorktreesByRepo(args.state), activeWorktreeId) ?? null)
|
||||
: null
|
||||
const activeGroupId = resolveCmdJActiveGroupId(
|
||||
args.state,
|
||||
@@ -168,7 +169,7 @@ export function buildCmdJQuickActionContext(args: {
|
||||
args.activeGroupSnapshot
|
||||
)
|
||||
const isLoading =
|
||||
args.state.repos.length > 0 && Object.keys(args.state.worktreesByRepo).length === 0
|
||||
args.state.repos.length > 0 && Object.keys(selectWorktreesByRepo(args.state)).length === 0
|
||||
const runtimeMode =
|
||||
(globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ &&
|
||||
args.state.settings?.activeRuntimeEnvironmentId?.trim()
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
startWorktreeAgentRowsCachePass,
|
||||
type WorktreeAgentRowsCache
|
||||
} from './worktree-agent-rows-cache'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
const EMPTY_COUNTS: Record<DashboardBucket, number> = {
|
||||
attention: 0,
|
||||
@@ -76,7 +77,7 @@ function selectActiveDashboardWorkspaces(
|
||||
if (
|
||||
memo &&
|
||||
memo.repos === state.repos &&
|
||||
memo.worktreesByRepo === state.worktreesByRepo &&
|
||||
memo.worktreesByRepo === selectWorktreesByRepo(state) &&
|
||||
memo.folderWorkspaces === state.folderWorkspaces &&
|
||||
memo.projectGroups === state.projectGroups
|
||||
) {
|
||||
@@ -86,7 +87,7 @@ function selectActiveDashboardWorkspaces(
|
||||
if (cache) {
|
||||
cache.activeWorkspaces = {
|
||||
repos: state.repos,
|
||||
worktreesByRepo: state.worktreesByRepo,
|
||||
worktreesByRepo: selectWorktreesByRepo(state),
|
||||
folderWorkspaces: state.folderWorkspaces,
|
||||
projectGroups: state.projectGroups,
|
||||
workspaces
|
||||
|
||||
@@ -18,6 +18,7 @@ import { folderWorkspaceToWorktree } from '../../../../shared/folder-workspace-w
|
||||
import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-overrides'
|
||||
import { isFolderRepo } from '../../../../shared/repo-kind'
|
||||
import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export type ActiveDashboardWorkspace = {
|
||||
projectId: string
|
||||
@@ -91,7 +92,7 @@ export function collectActiveDashboardWorkspaces(
|
||||
}
|
||||
|
||||
for (const repo of state.repos ?? []) {
|
||||
for (const worktree of state.worktreesByRepo?.[repo.id] ?? []) {
|
||||
for (const worktree of selectWorktreesByRepo(state)?.[repo.id] ?? []) {
|
||||
if (worktree.isArchived) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import type { TuiAgent } from '../../../../shared/tui-agent'
|
||||
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { getRuntimeAgentInventoryKey } from '@/store/slices/runtime-agent-inventory-key'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export type DashboardLaunchDetectionState = Pick<
|
||||
AppState,
|
||||
@@ -39,7 +40,7 @@ function buildDashboardLaunchCatalog(state: DashboardLaunchOptionState): Dashboa
|
||||
groupsById: new Map((state.projectGroups ?? []).map((group) => [group.id, group])),
|
||||
reposById: new Map((state.repos ?? []).map((repo) => [repo.id, repo])),
|
||||
worktreesByRepoAndId: new Map(
|
||||
Object.entries(state.worktreesByRepo ?? {}).map(([repoId, worktrees]) => [
|
||||
Object.entries(selectWorktreesByRepo(state) ?? {}).map(([repoId, worktrees]) => [
|
||||
repoId,
|
||||
new Map(worktrees.map((worktree) => [worktree.id, worktree]))
|
||||
])
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
buildDashboardBucketCounts,
|
||||
createDashboardBucketCountsCache
|
||||
} from './build-dashboard-bucket-counts'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export type AgentBucketCounts = Record<DashboardBucket, number>
|
||||
|
||||
@@ -51,7 +52,7 @@ export function selectAgentBucketCountState(s: AppState): AgentBucketCountState
|
||||
if (
|
||||
previous !== null &&
|
||||
previous.repos === s.repos &&
|
||||
previous.worktreesByRepo === s.worktreesByRepo &&
|
||||
selectWorktreesByRepo(previous) === selectWorktreesByRepo(s) &&
|
||||
previous.tabsByWorktree === s.tabsByWorktree &&
|
||||
previous.unifiedTabsByWorktree === s.unifiedTabsByWorktree &&
|
||||
previous.agentStatusByPaneKey === s.agentStatusByPaneKey &&
|
||||
@@ -62,7 +63,7 @@ export function selectAgentBucketCountState(s: AppState): AgentBucketCountState
|
||||
previous.ptyIdsByTabId === s.ptyIdsByTabId &&
|
||||
previous.runtimePaneTitlesByTabId === s.runtimePaneTitlesByTabId &&
|
||||
previous.paneForegroundAgentByPaneKey === s.paneForegroundAgentByPaneKey &&
|
||||
previous.folderWorkspaces === s.folderWorkspaces &&
|
||||
selectFolderWorkspaces(previous) === selectFolderWorkspaces(s) &&
|
||||
previous.acknowledgedAgentsByPaneKey === s.acknowledgedAgentsByPaneKey &&
|
||||
previous.agentStatusEpoch === s.agentStatusEpoch
|
||||
) {
|
||||
@@ -70,7 +71,7 @@ export function selectAgentBucketCountState(s: AppState): AgentBucketCountState
|
||||
}
|
||||
previousState = {
|
||||
repos: s.repos,
|
||||
worktreesByRepo: s.worktreesByRepo,
|
||||
worktreesByRepo: selectWorktreesByRepo(s),
|
||||
tabsByWorktree: s.tabsByWorktree,
|
||||
unifiedTabsByWorktree: s.unifiedTabsByWorktree,
|
||||
agentStatusByPaneKey: s.agentStatusByPaneKey,
|
||||
@@ -81,7 +82,7 @@ export function selectAgentBucketCountState(s: AppState): AgentBucketCountState
|
||||
ptyIdsByTabId: s.ptyIdsByTabId,
|
||||
runtimePaneTitlesByTabId: s.runtimePaneTitlesByTabId,
|
||||
paneForegroundAgentByPaneKey: s.paneForegroundAgentByPaneKey,
|
||||
folderWorkspaces: s.folderWorkspaces,
|
||||
folderWorkspaces: selectFolderWorkspaces(s),
|
||||
acknowledgedAgentsByPaneKey: s.acknowledgedAgentsByPaneKey,
|
||||
agentStatusEpoch: s.agentStatusEpoch,
|
||||
settings: null
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { RepoIcon } from '../../../../shared/repo-icon'
|
||||
import { buildDashboardSnapshot, type DashboardSnapshotState } from './build-dashboard-snapshot'
|
||||
import { createWorktreeAgentRowsCache } from './worktree-agent-rows-cache'
|
||||
import { launchDashboardAgent } from './launch-dashboard-agent'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
// Why: cap snapshot rebuilds during bursts of agent-status pings. The board is a
|
||||
// glanceable surface, so ~4 updates/sec is plenty and keeps the cross-worktree
|
||||
@@ -45,7 +46,7 @@ export function dashboardSnapshotInputsChanged(
|
||||
): boolean {
|
||||
return (
|
||||
state.repos !== previousState.repos ||
|
||||
state.worktreesByRepo !== previousState.worktreesByRepo ||
|
||||
selectWorktreesByRepo(state) !== selectWorktreesByRepo(previousState) ||
|
||||
state.tabsByWorktree !== previousState.tabsByWorktree ||
|
||||
state.unifiedTabsByWorktree !== previousState.unifiedTabsByWorktree ||
|
||||
state.retainedAgentsByPaneKey !== previousState.retainedAgentsByPaneKey ||
|
||||
|
||||
@@ -3,6 +3,11 @@ import { useAppStore } from '@/store'
|
||||
import type { DashboardSnapshot } from '../../../../shared/dashboard-snapshot'
|
||||
import { buildDashboardSnapshot } from './build-dashboard-snapshot'
|
||||
import { createWorktreeAgentRowsCache } from './worktree-agent-rows-cache'
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectFolderWorkspaces,
|
||||
selectWorktreesByRepo
|
||||
} from '@/store/workspace-record-selectors'
|
||||
|
||||
/**
|
||||
* Builds the dashboard snapshot directly from the live renderer store for the
|
||||
@@ -14,7 +19,7 @@ export function useLiveDashboardSnapshot(): DashboardSnapshot {
|
||||
const rowsCacheRef = useRef<ReturnType<typeof createWorktreeAgentRowsCache>>(undefined!)
|
||||
rowsCacheRef.current ??= createWorktreeAgentRowsCache()
|
||||
const repos = useAppStore((s) => s.repos)
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const worktreesByRepo = useAppStore((s) => selectWorktreesByRepo(s))
|
||||
const tabsByWorktree = useAppStore((s) => s.tabsByWorktree)
|
||||
const unifiedTabsByWorktree = useAppStore((s) => s.unifiedTabsByWorktree)
|
||||
const agentStatusByPaneKey = useAppStore((s) => s.agentStatusByPaneKey)
|
||||
@@ -40,10 +45,10 @@ export function useLiveDashboardSnapshot(): DashboardSnapshot {
|
||||
// bytes for the host the pty used to run on. All are low-frequency (each
|
||||
// writer bails out when nothing changed) next to agentStatusByPaneKey, which
|
||||
// already rebuilds this memo on every status ping.
|
||||
const detectedWorktreesByRepo = useAppStore((s) => s.detectedWorktreesByRepo)
|
||||
const detectedWorktreesByRepo = useAppStore((s) => selectDetectedWorktreesByRepo(s))
|
||||
// Why: a folder workspace is not a git worktree — its host resolves through
|
||||
// these two instead of worktreesByRepo.
|
||||
const folderWorkspaces = useAppStore((s) => s.folderWorkspaces)
|
||||
const folderWorkspaces = useAppStore((s) => selectFolderWorkspaces(s))
|
||||
const projectGroups = useAppStore((s) => s.projectGroups)
|
||||
const sshTargetLabels = useAppStore((s) => s.sshTargetLabels)
|
||||
const sshConnectionStates = useAppStore((s) => s.sshConnectionStates)
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
createWorktreeTabBucketProjection,
|
||||
type WorktreeTabBucketProjection
|
||||
} from '@/lib/worktree-tab-bucket-projection'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
// Why: when an agent finishes or its terminal closes, the store cleans up the
|
||||
// explicit status entry and the agent vanishes from the live status set.
|
||||
// Retaining the last-known "done" snapshot in the store lets the inline
|
||||
@@ -150,8 +152,8 @@ export function useRetainedAgentsSync(): void {
|
||||
(s) =>
|
||||
[
|
||||
s.repos,
|
||||
s.worktreesByRepo,
|
||||
s.folderWorkspaces,
|
||||
selectWorktreesByRepo(s),
|
||||
selectFolderWorkspaces(s),
|
||||
tabTopologyProjectionRef.current!.project(s.tabsByWorktree),
|
||||
s.agentStatusEpoch
|
||||
] as const
|
||||
@@ -163,8 +165,8 @@ export function useRetainedAgentsSync(): void {
|
||||
const state = useAppStore.getState()
|
||||
const { currentAgents, existingWorktreeIds, tabIndex } = buildRetainedAgentsSyncSnapshot({
|
||||
repos: state.repos,
|
||||
worktreesByRepo: state.worktreesByRepo,
|
||||
folderWorkspaces: state.folderWorkspaces,
|
||||
worktreesByRepo: selectWorktreesByRepo(state),
|
||||
folderWorkspaces: selectFolderWorkspaces(state),
|
||||
tabsByWorktree: state.tabsByWorktree,
|
||||
agentStatusByPaneKey: state.agentStatusByPaneKey,
|
||||
now: Date.now()
|
||||
|
||||
@@ -12,6 +12,7 @@ import { shouldShowEditorPanelHeader } from './editor-header'
|
||||
import { getUntitledFileRoot } from './untitled-file-rename-path'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { ArtifactWriteRequest } from '../../../../shared/artifacts'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
type EditorPanelRenderModel = ReturnType<typeof getEditorPanelRenderModel>
|
||||
|
||||
@@ -170,7 +171,7 @@ export function EditorPanelShell({
|
||||
? getUntitledFileRoot(
|
||||
renameDialogFile,
|
||||
findWorktreeById(
|
||||
useAppStore.getState().worktreesByRepo,
|
||||
selectWorktreesByRepo(useAppStore.getState()),
|
||||
renameDialogFile.worktreeId
|
||||
)?.path
|
||||
)
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { useIpynbCellExecution } from './useIpynbCellExecution'
|
||||
import { resolveNotebookExecutionHost } from './ipynb-execution-host'
|
||||
import { useIpynbScrollRestoration } from './useIpynbScrollRestoration'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
type IpynbViewerProps = {
|
||||
content: string
|
||||
@@ -53,7 +54,9 @@ export default function IpynbViewer({
|
||||
onSave
|
||||
}: IpynbViewerProps): React.JSX.Element {
|
||||
const editorFontZoomLevel = useAppStore((s) => s.editorFontZoomLevel)
|
||||
const rootPath = useAppStore((s) => findWorktreeById(s.worktreesByRepo, worktreeId)?.path ?? null)
|
||||
const rootPath = useAppStore(
|
||||
(s) => findWorktreeById(selectWorktreesByRepo(s), worktreeId)?.path ?? null
|
||||
)
|
||||
const executionHost = useAppStore((s) => resolveNotebookExecutionHost(s, worktreeId))
|
||||
const [editingCellKey, setEditingCellKey] = useState<string | null>(null)
|
||||
const parsed = useMemo(() => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { findWorktreeById } from '@/store/slices/worktree-helpers'
|
||||
import { getRuntimeGitRemoteFileUrl } from '@/runtime/runtime-git-working-tree-client'
|
||||
import { formatPathLineReference } from './line-copy-path'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
type MonacoGutterContextMenuProps = {
|
||||
open: boolean
|
||||
@@ -69,7 +70,7 @@ export function MonacoGutterContextMenu({
|
||||
if (!activeFile) {
|
||||
return
|
||||
}
|
||||
const worktree = findWorktreeById(state.worktreesByRepo, activeFile.worktreeId)
|
||||
const worktree = findWorktreeById(selectWorktreesByRepo(state), activeFile.worktreeId)
|
||||
if (!worktree) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getOpenableAnnotationLine,
|
||||
resolveAnnotationPathInsideWorktree
|
||||
} from './check-annotation-path'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export { getOpenableAnnotationLine }
|
||||
|
||||
@@ -18,7 +19,7 @@ export function openAnnotationLocation(params: {
|
||||
}): void {
|
||||
const { worktreeId, path, line, revealRafRef, revealInnerRafRef } = params
|
||||
const store = useAppStore.getState()
|
||||
const worktree = findWorktreeById(store.worktreesByRepo, worktreeId)
|
||||
const worktree = findWorktreeById(selectWorktreesByRepo(store), worktreeId)
|
||||
if (!worktree) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/github
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { isGitHubPRSuppressed } from '../../../../shared/worktree/github-pr-suppression'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export function resolveCheckRunDetailsFixCheck(
|
||||
check: PRCheckDetail,
|
||||
@@ -40,7 +41,7 @@ export function resolveHostedReviewForCheckRunDetailsFix(
|
||||
worktreeId: string
|
||||
): HostedReviewInfo | null {
|
||||
const store = useAppStore.getState()
|
||||
const worktree = findWorktreeById(store.worktreesByRepo, worktreeId)
|
||||
const worktree = findWorktreeById(selectWorktreesByRepo(store), worktreeId)
|
||||
if (!worktree) {
|
||||
return null
|
||||
}
|
||||
@@ -121,7 +122,7 @@ export function getCheckRunDetailsFixDisabledReason(worktreeId: string | null):
|
||||
)
|
||||
}
|
||||
const store = useAppStore.getState()
|
||||
const worktree = findWorktreeById(store.worktreesByRepo, worktreeId)
|
||||
const worktree = findWorktreeById(selectWorktreesByRepo(store), worktreeId)
|
||||
if (!worktree) {
|
||||
return translate(
|
||||
'auto.components.editor.check.run.details.fix.with.ai.1a8c4e2b90',
|
||||
@@ -149,7 +150,7 @@ export function resolveCheckRunDetailsFixRepo(worktreeId: string | null): Repo |
|
||||
return null
|
||||
}
|
||||
const store = useAppStore.getState()
|
||||
const worktree = findWorktreeById(store.worktreesByRepo, worktreeId)
|
||||
const worktree = findWorktreeById(selectWorktreesByRepo(store), worktreeId)
|
||||
if (!worktree) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import type {
|
||||
} from '../../../../shared/source-control-ai-actions'
|
||||
import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/github/check-types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export {
|
||||
buildCheckRunDetailsFixBasePrompt,
|
||||
@@ -130,7 +131,7 @@ export function useCheckRunDetailsFixWithAI(args: {
|
||||
if (!args.worktreeId) {
|
||||
return null
|
||||
}
|
||||
return findWorktreeById(useAppStore.getState().worktreesByRepo, args.worktreeId)
|
||||
return findWorktreeById(selectWorktreesByRepo(useAppStore.getState()), args.worktreeId)
|
||||
}, [args.worktreeId])
|
||||
const canFixWithAI = isCheckRunDetailsFixCandidate(args.check, args.details)
|
||||
const disabledReason = getCheckRunDetailsFixDisabledReason(args.worktreeId)
|
||||
|
||||
+2
-1
@@ -21,6 +21,7 @@ import type { DiffSection } from '../../diff-section-types'
|
||||
import type { DiffSectionItemProps } from '../../diff-section-item-props'
|
||||
import { getDiskBaselineSignature } from '../../../../../../shared/disk-baseline-signature'
|
||||
import { writeEditorSave } from '../../editor-save-disk-precondition'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export type CombinedDiffSectionActions = {
|
||||
handleSectionSaveRef: DiffSectionItemProps['handleSectionSaveRef']
|
||||
@@ -191,7 +192,7 @@ export function useCombinedDiffSectionActions({
|
||||
try {
|
||||
const state = useAppStore.getState()
|
||||
const worktree = file.worktreeId
|
||||
? findWorktreeById(state.worktreesByRepo, file.worktreeId)
|
||||
? findWorktreeById(selectWorktreesByRepo(state), file.worktreeId)
|
||||
: null
|
||||
const writeResult = await writeEditorSave(
|
||||
getEditorFileOperationContext(
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
type EditorSaveResult,
|
||||
type EditorSaveTrigger
|
||||
} from './editor-save-disk-precondition'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export type AppStoreApi = Pick<StoreApi<AppState>, 'getState' | 'subscribe'>
|
||||
|
||||
@@ -159,7 +160,7 @@ export function createEditorSaveQueue(store: AppStoreApi): EditorSaveQueue {
|
||||
return 'failed'
|
||||
}
|
||||
const worktree = liveFile.worktreeId
|
||||
? findWorktreeById(state.worktreesByRepo ?? {}, liveFile.worktreeId)
|
||||
? findWorktreeById(selectWorktreesByRepo(state) ?? {}, liveFile.worktreeId)
|
||||
: null
|
||||
const fileContext = getEditorFileOperationContext(state, liveFile, worktree?.path ?? null)
|
||||
const connectionId = fileContext.connectionId
|
||||
|
||||
@@ -15,6 +15,7 @@ import { buildRichMarkdownImageInsertContent } from './rich-markdown-image-inser
|
||||
import { showRichMarkdownImageInsertionCanceled } from './rich-markdown-image-insertion-feedback'
|
||||
import type { RichMarkdownImageInsertionRange } from './rich-markdown-image-insertion-target'
|
||||
import { richMarkdownClipboardInsertionOrderKey } from './rich-markdown-clipboard-insertion-order'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export type RichMarkdownImageInsertArgs = {
|
||||
editor: Editor
|
||||
@@ -140,11 +141,11 @@ function getWorktreePath(worktreeId: string | null): string | null {
|
||||
const parsedWorkspaceKey = parseWorkspaceKey(worktreeId)
|
||||
if (parsedWorkspaceKey?.type === 'folder') {
|
||||
return (
|
||||
state.folderWorkspaces.find(
|
||||
selectFolderWorkspaces(state).find(
|
||||
(workspace) => workspace.id === parsedWorkspaceKey.folderWorkspaceId
|
||||
)?.folderPath ?? null
|
||||
)
|
||||
}
|
||||
const worktrees = Object.values(state.worktreesByRepo ?? {}).flat()
|
||||
const worktrees = Object.values(selectWorktreesByRepo(state) ?? {}).flat()
|
||||
return worktrees.find((worktree) => worktree.id === worktreeId)?.path ?? null
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from './markdown-preview-source-routing'
|
||||
import { useDocumentDarkTheme } from '@/hooks/use-document-dark-theme'
|
||||
import { usePreserveSectionDuringExternalEdit } from './usePreserveSectionDuringExternalEdit'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export function useMarkdownPreviewSourceFoundation({
|
||||
content,
|
||||
@@ -65,7 +66,7 @@ export function useMarkdownPreviewSourceFoundation({
|
||||
const updateDiffComment = useAppStore((s) => s.updateDiffComment)
|
||||
const clearDeliveredDiffComments = useAppStore((s) => s.clearDeliveredDiffComments)
|
||||
const keybindings = useAppStore((s) => s.keybindings)
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const worktreesByRepo = useAppStore((s) => selectWorktreesByRepo(s))
|
||||
const sourceOpenFile = useAppStore((s) =>
|
||||
findMarkdownPreviewSourceOpenFile(s.openFiles, {
|
||||
sourceFileId,
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { HttpLinkSourceOwner } from '@/lib/http-link-routing'
|
||||
import { createRichMarkdownEditorCodec } from './rich-markdown-source-transport'
|
||||
import { createRichMarkdownHtmlSuperscriptLinkContext } from './rich-markdown-html-superscript-link-context'
|
||||
import type { AppState } from '@/store/types'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export function resolveRichMarkdownWorktreeRoot(
|
||||
state: Pick<AppState, 'folderWorkspaces' | 'worktreesByRepo'>,
|
||||
@@ -14,9 +15,10 @@ export function resolveRichMarkdownWorktreeRoot(
|
||||
): string | null {
|
||||
const workspaceScope = parseWorkspaceKey(worktreeId)
|
||||
return workspaceScope?.type === 'folder'
|
||||
? (state.folderWorkspaces.find((workspace) => workspace.id === workspaceScope.folderWorkspaceId)
|
||||
?.folderPath ?? null)
|
||||
: (getIndexedWorktreeById(state.worktreesByRepo, worktreeId)?.path ?? null)
|
||||
? (selectFolderWorkspaces(state).find(
|
||||
(workspace) => workspace.id === workspaceScope.folderWorkspaceId
|
||||
)?.folderPath ?? null)
|
||||
: (getIndexedWorktreeById(selectWorktreesByRepo(state), worktreeId)?.path ?? null)
|
||||
}
|
||||
|
||||
export function useRichMarkdownSuperscriptLinkSetup({
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import type { ExecutionHostId } from '../../../shared/execution-host'
|
||||
import { useAppStore } from '../store'
|
||||
import type { ResumeWorkspaceGroup } from './native-chat-resume-on-restart-grouping'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
// Store lookups the resume tree makes for its workspaces, each on the workspace's own host.
|
||||
|
||||
@@ -50,7 +51,7 @@ export function useRepoIdByWorkspace(
|
||||
export function useLineageAncestors(
|
||||
workspaces: readonly ResumeWorkspaceGroup[]
|
||||
): (group: ResumeWorkspaceGroup) => readonly string[] {
|
||||
const worktreesByRepo = useAppStore((store) => store.worktreesByRepo)
|
||||
const worktreesByRepo = useAppStore((store) => selectWorktreesByRepo(store))
|
||||
const worktreeLineageById = useAppStore((store) => store.worktreeLineageById)
|
||||
const ancestors = useMemo(() => {
|
||||
const state = { worktreesByRepo }
|
||||
|
||||
@@ -14,6 +14,11 @@ import { isFloatingWorkspaceId } from '../../../../shared/floating-workspace-wor
|
||||
import { resolveNativeChatTabDirectory } from './native-chat-tab-directory'
|
||||
import { useMemo } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectFolderWorkspaces,
|
||||
selectWorktreesByRepo
|
||||
} from '@/store/workspace-record-selectors'
|
||||
|
||||
/** The transcript must not read until ownership and its path are both known. */
|
||||
export type NativeChatImageRuntimeContext = RuntimeFileOperationArgs | null
|
||||
@@ -46,9 +51,9 @@ export function selectNativeChatImageOwnerState(state: AppState): OwnerState {
|
||||
return {
|
||||
settings: state.settings,
|
||||
repos: state.repos,
|
||||
worktreesByRepo: state.worktreesByRepo,
|
||||
detectedWorktreesByRepo: state.detectedWorktreesByRepo,
|
||||
folderWorkspaces: state.folderWorkspaces,
|
||||
worktreesByRepo: selectWorktreesByRepo(state),
|
||||
detectedWorktreesByRepo: selectDetectedWorktreesByRepo(state),
|
||||
folderWorkspaces: selectFolderWorkspaces(state),
|
||||
floatingWorkspacePath: state.floatingWorkspacePath,
|
||||
structuredSessionLaunchDirectoryByTabId: state.structuredSessionLaunchDirectoryByTabId,
|
||||
projectGroups: state.projectGroups,
|
||||
|
||||
@@ -10,6 +10,11 @@ import {
|
||||
resolveNativeChatTabDirectoryResolution,
|
||||
type NativeChatTabDirectoryState
|
||||
} from './native-chat-tab-directory'
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectFolderWorkspaces,
|
||||
selectWorktreesByRepo
|
||||
} from '@/store/workspace-record-selectors'
|
||||
|
||||
export type NativeChatSkillStateInputs = Pick<
|
||||
AppState,
|
||||
@@ -50,9 +55,9 @@ export function selectNativeChatSkillStateInputs(state: AppState): NativeChatSki
|
||||
// Why: the owner resolver needs the picked host and detected rows to tell twins apart.
|
||||
activeWorkspaceExecutionHostId: state.activeWorkspaceExecutionHostId,
|
||||
activeWorktreeId: state.activeWorktreeId,
|
||||
detectedWorktreesByRepo: state.detectedWorktreesByRepo,
|
||||
detectedWorktreesByRepo: selectDetectedWorktreesByRepo(state),
|
||||
floatingWorkspacePath: state.floatingWorkspacePath,
|
||||
folderWorkspaces: state.folderWorkspaces,
|
||||
folderWorkspaces: selectFolderWorkspaces(state),
|
||||
projectGroups: state.projectGroups,
|
||||
projects: state.projects,
|
||||
repos: state.repos,
|
||||
@@ -61,7 +66,7 @@ export function selectNativeChatSkillStateInputs(state: AppState): NativeChatSki
|
||||
structuredSessionLaunchDirectoryByTabId: state.structuredSessionLaunchDirectoryByTabId,
|
||||
tabsByWorktree: state.tabsByWorktree,
|
||||
unifiedTabsByWorktree: state.unifiedTabsByWorktree,
|
||||
worktreesByRepo: state.worktreesByRepo
|
||||
worktreesByRepo: selectWorktreesByRepo(state)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
type ExecutionHostId
|
||||
} from '../../../../shared/execution-host'
|
||||
import { useRepoMap } from '@/store/selectors'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
/** Single-line row: text-sm leading (20px) over py-2. Set as the row's explicit height. */
|
||||
const NO_PARENT_ROW_HEIGHT = 36
|
||||
@@ -96,7 +97,7 @@ function ComposerParentWorktreePickerImpl({
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
const parent = getIndexedWorktreeById(s.worktreesByRepo, value)
|
||||
const parent = getIndexedWorktreeById(selectWorktreesByRepo(s), value)
|
||||
return parent && !parent.isArchived ? parent.displayName : null
|
||||
})
|
||||
|
||||
@@ -214,7 +215,7 @@ function ParentWorktreeCandidateList({
|
||||
activeFolderWorkspaceId,
|
||||
onSelect
|
||||
}: ParentWorktreeCandidateListProps): React.JSX.Element {
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const worktreesByRepo = useAppStore((s) => selectWorktreesByRepo(s))
|
||||
const repoMap = useRepoMap()
|
||||
const worktreeLineageById = useAppStore((s) => s.worktreeLineageById)
|
||||
const workspaceLineageByChildKey = useAppStore((s) => s.workspaceLineageByChildKey)
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
} from './use-ai-vault-session-keyboard-navigation'
|
||||
import { useAiVaultPanelHandle, type AiVaultPanelProps } from './ai-vault-panel-handle'
|
||||
import { aiVaultSearchHitHostLabels } from './ai-vault-search-hit-hosts'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export default function AiVaultPanel({
|
||||
ref,
|
||||
@@ -70,10 +71,10 @@ export default function AiVaultPanel({
|
||||
useShallow((state) => ({
|
||||
projects: state.projects,
|
||||
settings: state.settings,
|
||||
folderWorkspaces: state.folderWorkspaces,
|
||||
folderWorkspaces: selectFolderWorkspaces(state),
|
||||
projectGroups: state.projectGroups,
|
||||
repos: state.repos,
|
||||
worktreesByRepo: state.worktreesByRepo
|
||||
worktreesByRepo: selectWorktreesByRepo(state)
|
||||
}))
|
||||
)
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
runLimitedParentPrChecksRefreshes
|
||||
} from './parent-pr-checks-refresh'
|
||||
import { createParentPrChecksProjectionSelector } from './parent-pr-checks-projection-selector'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
type FolderWorkspacePrChecksPanelProps = {
|
||||
isVisible?: boolean
|
||||
@@ -24,10 +25,10 @@ export default function FolderWorkspacePrChecksPanel({
|
||||
}: FolderWorkspacePrChecksPanelProps): React.JSX.Element {
|
||||
const activeWorktreeId = useAppStore((s) => s.activeWorktreeId)
|
||||
const activeWorkspaceKey = useAppStore((s) => s.activeWorkspaceKey)
|
||||
const folderWorkspaces = useAppStore((s) => s.folderWorkspaces)
|
||||
const folderWorkspaces = useAppStore((s) => selectFolderWorkspaces(s))
|
||||
const workspaceLineageByChildKey = useAppStore((s) => s.workspaceLineageByChildKey)
|
||||
const worktreeLineageById = useAppStore((s) => s.worktreeLineageById)
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const worktreesByRepo = useAppStore((s) => selectWorktreesByRepo(s))
|
||||
const repos = useAppStore((s) => s.repos)
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const fetchHostedReviewForBranch = useAppStore((s) => s.fetchHostedReviewForBranch)
|
||||
|
||||
@@ -9,14 +9,15 @@ import { translate } from '@/i18n/i18n'
|
||||
import type { Worktree } from '../../../../shared/worktree/types'
|
||||
import { getAttachedWorktreesForFolderWorkspace } from './folder-workspace-attached-worktrees'
|
||||
import { useState } from 'react'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export default function FolderWorkspaceWorktreesPanel(): React.JSX.Element {
|
||||
const activeWorktreeId = useAppStore((s) => s.activeWorktreeId)
|
||||
const activeWorkspaceKey = useAppStore((s) => s.activeWorkspaceKey)
|
||||
const folderWorkspaces = useAppStore((s) => s.folderWorkspaces)
|
||||
const folderWorkspaces = useAppStore((s) => selectFolderWorkspaces(s))
|
||||
const workspaceLineageByChildKey = useAppStore((s) => s.workspaceLineageByChildKey)
|
||||
const worktreeLineageById = useAppStore((s) => s.worktreeLineageById)
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const worktreesByRepo = useAppStore((s) => selectWorktreesByRepo(s))
|
||||
const repos = useAppStore((s) => s.repos)
|
||||
const [collapsedLineageWorktreeIds, setCollapsedLineageWorktreeIds] = useState<
|
||||
ReadonlySet<string>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { buildAiVaultProjectContext } from './ai-vault-session-projects'
|
||||
import { selectAiVaultPanelWorkspace } from './ai-vault-panel-workspace'
|
||||
import type { AiVaultSessionListRequest } from './ai-vault-session-list-request'
|
||||
import { readAiVaultViewOptions } from './ai-vault-view-options-persistence'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
/** The request the panel sends by default while `workspaceId` is the active workspace, read from
|
||||
* the store through the same selectors and derivations the panel's hooks use. */
|
||||
@@ -17,7 +18,7 @@ export function resolveAiVaultPanelSessionListRequest(
|
||||
workspaceId: string
|
||||
): AiVaultSessionListRequest {
|
||||
const { worktree, repo } = selectAiVaultPanelWorkspace(state, { worktreeId: workspaceId })
|
||||
const allWorktrees = getIndexedAllWorktrees(state.worktreesByRepo)
|
||||
const allWorktrees = getIndexedAllWorktrees(selectWorktreesByRepo(state))
|
||||
const projectHostSetupProjection = getProjectHostSetupProjectionFromState(state)
|
||||
const { activeProjectKey } = buildAiVaultProjectContext({
|
||||
repos: state.repos,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
isKnownAiVaultResumeWorkspaceTarget,
|
||||
type AiVaultSessionResumeTargetState
|
||||
} from './ai-vault-session-resume'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export function resolveAiVaultTargetWorkspacePath(
|
||||
state: AiVaultSessionResumeTargetState,
|
||||
@@ -20,12 +21,12 @@ export function resolveAiVaultTargetWorkspacePath(
|
||||
const scope = parseWorkspaceKey(workspaceId)
|
||||
if (scope?.type === 'folder') {
|
||||
return (
|
||||
state.folderWorkspaces.find((workspace) => workspace.id === scope.folderWorkspaceId)
|
||||
selectFolderWorkspaces(state).find((workspace) => workspace.id === scope.folderWorkspaceId)
|
||||
?.folderPath ?? null
|
||||
)
|
||||
}
|
||||
const worktreeId = scope?.type === 'worktree' ? scope.worktreeId : workspaceId
|
||||
return findWorktreeById(state.worktreesByRepo, worktreeId)?.path ?? null
|
||||
return findWorktreeById(selectWorktreesByRepo(state), worktreeId)?.path ?? null
|
||||
}
|
||||
|
||||
export type AiVaultSessionLaunchTarget =
|
||||
|
||||
@@ -7,16 +7,17 @@ import { translate } from '@/i18n/i18n'
|
||||
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import type { AiVaultSession } from '../../../../shared/ai-vault-types'
|
||||
import { canOpenAiVaultSessionLogInOrca } from './ai-vault-session-path-actions'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
type AiVaultLogSession = Pick<AiVaultSession, 'filePath' | 'executionHostId'>
|
||||
|
||||
function worktreeStillExists(state: AppState, worktreeId: string): boolean {
|
||||
if (findWorktreeById(state.worktreesByRepo ?? {}, worktreeId)) {
|
||||
if (findWorktreeById(selectWorktreesByRepo(state) ?? {}, worktreeId)) {
|
||||
return true
|
||||
}
|
||||
// Why: the AI Vault panel can be active inside a folder workspace, whose id is
|
||||
// keyed differently from repo worktrees.
|
||||
return (state.folderWorkspaces ?? []).some(
|
||||
return (selectFolderWorkspaces(state) ?? []).some(
|
||||
(workspace) => folderWorkspaceKey(workspace.id) === worktreeId
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
resolveAiVaultSessionWorktreeInfo,
|
||||
type AiVaultSessionWorktreeInfo
|
||||
} from './ai-vault-session-worktree'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export type AiVaultSessionResumeTargetState = Pick<
|
||||
AppState,
|
||||
@@ -171,13 +172,13 @@ export function isKnownAiVaultResumeWorkspaceTarget(
|
||||
|
||||
const workspaceKey = parseWorkspaceKey(workspaceId)
|
||||
if (workspaceKey?.type === 'folder') {
|
||||
return state.folderWorkspaces.some(
|
||||
return selectFolderWorkspaces(state).some(
|
||||
(workspace) => workspace.id === workspaceKey.folderWorkspaceId
|
||||
)
|
||||
}
|
||||
|
||||
const worktreeId = workspaceKey?.type === 'worktree' ? workspaceKey.worktreeId : workspaceId
|
||||
return getIndexedWorktreeMap(state.worktreesByRepo).has(worktreeId)
|
||||
return getIndexedWorktreeMap(selectWorktreesByRepo(state)).has(worktreeId)
|
||||
}
|
||||
|
||||
function resolveSupportedResumeWorktreeId(args: {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { fileExplorerRefreshConcurrency } from './file-explorer-refresh-concurrency'
|
||||
import { createFileExplorerWatchRefreshScheduler } from './file-explorer-watch-refresh-scheduler'
|
||||
import { processFileExplorerFsPayload } from './file-explorer-watch-reconcile'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export {
|
||||
canonicalizeFileExplorerWatchPath,
|
||||
@@ -58,7 +59,7 @@ export function getFileExplorerWatchRuntimeEnvironmentId(
|
||||
activeWorkspaceExecutionHostId: state.activeWorkspaceExecutionHostId,
|
||||
settings: state.settings,
|
||||
repos: state.repos,
|
||||
worktreesByRepo: state.worktreesByRepo,
|
||||
worktreesByRepo: selectWorktreesByRepo(state),
|
||||
detectedWorktreesByRepo: state.detectedWorktreesByRepo ?? {},
|
||||
folderWorkspaces: state.folderWorkspaces ?? [],
|
||||
projectGroups: state.projectGroups ?? [],
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type GitStatusRefreshReason,
|
||||
type GitStatusRefreshScheduler
|
||||
} from './git-status-refresh-scheduler'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
const STATUS_SAFETY_INTERVAL_MS = 60_000
|
||||
const STATUS_ACTIVITY_DEBOUNCE_MS = 125
|
||||
@@ -295,7 +296,9 @@ export function useGitStatusPolling(options: { enabled?: boolean } = {}): void {
|
||||
|
||||
const getRepoWorktreePathsForSignal = useCallback(
|
||||
(repoId: string) =>
|
||||
(useAppStore.getState().worktreesByRepo[repoId] ?? []).map((worktree) => worktree.path),
|
||||
(selectWorktreesByRepo(useAppStore.getState())[repoId] ?? []).map(
|
||||
(worktree) => worktree.path
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
} from '../../../../shared/file-write-disk-precondition'
|
||||
import type { RuntimeFileOperationArgs } from '@/runtime/runtime-file-client-types'
|
||||
import { runtimeTargetForOwnerEnvironment } from '@/runtime/runtime-client-target'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
type McpConfigSectionProps = {
|
||||
repo: Repo
|
||||
@@ -58,7 +59,7 @@ export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Ele
|
||||
const activeWorktreeId = useAppStore((state) => state.activeWorktreeId)
|
||||
const repos = useAppStore((state) => state.repos)
|
||||
const allWorktreesForRepo = useAppStore(
|
||||
(state) => state.worktreesByRepo[repo.id] ?? EMPTY_WORKTREES
|
||||
(state) => selectWorktreesByRepo(state)[repo.id] ?? EMPTY_WORKTREES
|
||||
)
|
||||
// Why: a path only means something on the host that owns this repo row, not the focused one.
|
||||
const repoHostId = getRepoExecutionHostId(repo)
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
getSetupGuideProgressReady,
|
||||
getSetupScriptProbeSignature
|
||||
} from './setup-guide-progress-readiness'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
import {
|
||||
readSetupScriptProbeCache,
|
||||
setSetupScriptProbeCache,
|
||||
@@ -45,7 +46,7 @@ export function useSetupGuideProgress(
|
||||
): FeatureWallSetupProgress {
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const featureInteractions = useAppStore((s) => s.featureInteractions)
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const worktreesByRepo = useAppStore((s) => selectWorktreesByRepo(s))
|
||||
const preflightStatus = useAppStore((s) => s.preflightStatus)
|
||||
const preflightStatusChecked = useAppStore((s) => s.preflightStatusChecked)
|
||||
const preflightStatusContextKey = useAppStore((s) => s.preflightStatusContextKey)
|
||||
|
||||
@@ -22,6 +22,7 @@ import type { HostRemovalTarget } from './host-rename-remove'
|
||||
import { resolveSshHostRemoval } from './ssh-host-remove-resolution'
|
||||
import { clearSshHostWorkspaces } from './ssh-host-remove-workspaces'
|
||||
import { removeWslHost } from './wsl-host-removal'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
type HostRemoveDialogProps = {
|
||||
open: boolean
|
||||
@@ -48,7 +49,7 @@ export function HostRemoveDialog({
|
||||
const mountedRef = useMountedRef()
|
||||
|
||||
const repos = useAppStore((s) => s.repos)
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const worktreesByRepo = useAppStore((s) => selectWorktreesByRepo(s))
|
||||
const sshConnectionStates = useAppStore((s) => s.sshConnectionStates)
|
||||
|
||||
// Resolve which workspaces live on the target so the dialog can offer to
|
||||
|
||||
@@ -7,6 +7,7 @@ import { translate } from '@/i18n/i18n'
|
||||
import { useAppStore } from '@/store'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
|
||||
import { findLocalToolchainBlock, type LocalToolchainFailureKind } from './repo-scan-failure'
|
||||
import { selectDetectedWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
const FIXES: Record<
|
||||
LocalToolchainFailureKind,
|
||||
@@ -43,14 +44,15 @@ const FIXES: Record<
|
||||
/** One sidebar-level notice for local Git toolchain failures that block every local repo at once. */
|
||||
export function LocalGitToolchainScanBanner(): React.JSX.Element | null {
|
||||
const repos = useAppStore((s) => s.repos)
|
||||
const detectedByRepo = useAppStore((s) => s.detectedWorktreesByRepo)
|
||||
const detectedByRepo = useAppStore((s) => selectDetectedWorktreesByRepo(s))
|
||||
const block = findLocalToolchainBlock(repos, detectedByRepo)
|
||||
const [pending, setPending] = React.useState(false)
|
||||
|
||||
// Why: reads the store at call time so the focus listener below never needs re-subscribing.
|
||||
const rescan = React.useCallback(async () => {
|
||||
const state = useAppStore.getState()
|
||||
const blocked = findLocalToolchainBlock(state.repos, state.detectedWorktreesByRepo)?.repos ?? []
|
||||
const blocked =
|
||||
findLocalToolchainBlock(state.repos, selectDetectedWorktreesByRepo(state))?.repos ?? []
|
||||
if (blocked.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { upsertAddedRepoWithProjectHostSetup } from './add-repo-store-upsert'
|
||||
import { worktreeRefreshOptions } from './add-repo-runtime-owner'
|
||||
import { addRuntimeSshRepo } from '@/runtime/runtime-ssh-target-management'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
// Why: the SSH target is a paired server's own host, and only that server adds a project on it.
|
||||
async function addSshFolderProject(args: {
|
||||
@@ -100,11 +101,9 @@ const NonGitFolderDialog = React.memo(function NonGitFolderDialog() {
|
||||
// dialog closes and the UI shows no visible change, making the
|
||||
// add feel like a no-op. Activating the synthetic folder
|
||||
// worktree reveals it in the sidebar and opens the workspace.
|
||||
const folderWorktree = useAppStore
|
||||
.getState()
|
||||
.worktreesByRepo[repo.id]?.find(
|
||||
(worktree) => worktree.hostId === ownerOptions.executionHostId
|
||||
)
|
||||
const folderWorktree = selectWorktreesByRepo(useAppStore.getState())[repo.id]?.find(
|
||||
(worktree) => worktree.hostId === ownerOptions.executionHostId
|
||||
)
|
||||
if (folderWorktree) {
|
||||
const onboarding = await window.api.onboarding.get().catch(() => null)
|
||||
// Why: SSH users can hit this dialog from Add Project after
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useAppStore } from '@/store'
|
||||
import { activateAndRevealWorktree } from '@/lib/worktree-activation'
|
||||
import { isFolderRepo } from '../../../../shared/repo-kind'
|
||||
import { finishProjectAddWithDefaultCheckout } from './project-added-default-checkout'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
type ProjectAddedModalData = {
|
||||
repoId?: string
|
||||
@@ -80,7 +81,7 @@ export default function ProjectAddedDialog(): null {
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
const folderWorktree = useAppStore.getState().worktreesByRepo[repoId]?.[0]
|
||||
const folderWorktree = selectWorktreesByRepo(useAppStore.getState())[repoId]?.[0]
|
||||
if (folderWorktree) {
|
||||
activateAndRevealWorktree(folderWorktree.id, { sidebarRevealBehavior: 'auto' })
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '../../../../shared/execution-host'
|
||||
import { countOpenRepoTerminalTabs } from '@/store/repos/repo-removal'
|
||||
import { getRecipeVmSshTargetForHost } from '@/lib/recipe-vm-managed-host'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
// Why: interpolated into the sentence so locales control where the name sits;
|
||||
// U+0000 cannot appear in a real project name, so the split is unambiguous.
|
||||
@@ -58,7 +59,7 @@ const RemoveFolderDialog = React.memo(function RemoveFolderDialog() {
|
||||
const onRecipeVmServer = useAppStore(
|
||||
(s) =>
|
||||
getRecipeVmSshTargetForHost(s.runtimeEnvironments, hostId) !== null ||
|
||||
(s.worktreesByRepo[repoId] ?? []).some(
|
||||
(selectWorktreesByRepo(s)[repoId] ?? []).some(
|
||||
(worktree) =>
|
||||
worktree.hostId === hostId && worktree.ephemeralVmCheckoutMode === 'provisioned-root'
|
||||
)
|
||||
|
||||
@@ -10,6 +10,11 @@ import { ActivityThreadListPane } from '@/components/activity/activity-thread-li
|
||||
import { useAgentPaneThreads } from '@/components/activity/use-agent-pane-threads'
|
||||
import { ActivityThreadOptionsMenu } from '@/components/activity/activity-thread-controls'
|
||||
import type { ActivityGroupBy, ThreadReadFilter } from '@/components/activity/activity-thread-types'
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectFolderWorkspaces,
|
||||
selectWorktreesByRepo
|
||||
} from '@/store/workspace-record-selectors'
|
||||
|
||||
/**
|
||||
* The Activity thread list, hosted in the sidebar as a navigator: selecting a
|
||||
@@ -96,19 +101,23 @@ export default function SidebarAgentsList({
|
||||
setSelectedPaneKey
|
||||
})
|
||||
|
||||
const worktreesByRepo = selectWorktreesByRepo(storeData)
|
||||
const detectedWorktreesByRepo = selectDetectedWorktreesByRepo(storeData)
|
||||
const folderWorkspaces = selectFolderWorkspaces(storeData)
|
||||
|
||||
const canJumpToWorkspace = useCallback(
|
||||
(thread: Parameters<typeof hasActivityThreadWorkspace>[0]) =>
|
||||
hasActivityThreadWorkspace(thread, {
|
||||
worktreesByRepo: storeData.worktreesByRepo,
|
||||
detectedWorktreesByRepo: storeData.detectedWorktreesByRepo,
|
||||
folderWorkspaces: storeData.folderWorkspaces,
|
||||
worktreesByRepo,
|
||||
detectedWorktreesByRepo,
|
||||
folderWorkspaces,
|
||||
floatingWorkspacePath: storeData.floatingWorkspacePath,
|
||||
defaultHostId: storeData.defaultHostId
|
||||
}),
|
||||
[
|
||||
storeData.worktreesByRepo,
|
||||
storeData.detectedWorktreesByRepo,
|
||||
storeData.folderWorkspaces,
|
||||
worktreesByRepo,
|
||||
detectedWorktreesByRepo,
|
||||
folderWorkspaces,
|
||||
storeData.floatingWorkspacePath,
|
||||
storeData.defaultHostId
|
||||
]
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useWorkspaceKanbanNativeDrag } from './use-workspace-kanban-native-drag
|
||||
import { useWorkspaceKanbanRenderLifecycle } from './use-workspace-kanban-render-lifecycle'
|
||||
import { useWorkspaceKanbanDrawerLingering } from './use-workspace-kanban-drawer-lingering'
|
||||
import { buildWorktreeManualOrderCatalog } from './worktree-manual-order-catalog'
|
||||
import { selectFolderWorkspaces } from '@/store/workspace-record-selectors'
|
||||
|
||||
type WorkspaceKanbanDrawerProps = {
|
||||
leftSidebarStyle?: React.CSSProperties
|
||||
@@ -53,7 +54,7 @@ function WorkspaceKanbanDrawerContent({
|
||||
onMenuOpenChange
|
||||
}: WorkspaceKanbanDrawerProps): React.JSX.Element {
|
||||
const allWorktrees = useAllWorktrees()
|
||||
const folderWorkspaces = useAppStore((s) => s.folderWorkspaces)
|
||||
const folderWorkspaces = useAppStore((s) => selectFolderWorkspaces(s))
|
||||
const repoMap = useRepoMap()
|
||||
const activeWorktreeId = useAppStore((s) => s.activeWorktreeId)
|
||||
const activeWorkspaceExecutionHostId = useAppStore((s) => s.activeWorkspaceExecutionHostId)
|
||||
|
||||
@@ -35,6 +35,10 @@ import { ORCA_SIDEBAR_INPUTS_IS_MAIN } from '../../../../shared/sidebar-inputs-s
|
||||
import { useWorktreeStatusMutations } from './worktree-list/drag/use-status-mutations'
|
||||
import { shouldFiltersHideAllRows } from './sidebar-empty-state-gate'
|
||||
import { buildWorktreeManualOrderCatalog } from './worktree-manual-order-catalog'
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectFolderWorkspaces
|
||||
} from '@/store/workspace-record-selectors'
|
||||
|
||||
type WorktreeListProps = {
|
||||
scrollOffsetRef: React.MutableRefObject<number>
|
||||
@@ -68,7 +72,7 @@ const WorktreeList = React.memo(function WorktreeList({
|
||||
const repos = useAppStore((s) => s.repos)
|
||||
const worktreeLineageById = useAppStore((s) => s.worktreeLineageById)
|
||||
const workspaceLineageByChildKey = useAppStore((s) => s.workspaceLineageByChildKey)
|
||||
const detectedWorktreesByRepo = useAppStore((s) => s.detectedWorktreesByRepo)
|
||||
const detectedWorktreesByRepo = useAppStore((s) => selectDetectedWorktreesByRepo(s))
|
||||
const activeWorktreeId = useAppStore((s) => s.activeWorktreeId)
|
||||
const activeWorkspaceExecutionHostId = useAppStore((s) => s.activeWorkspaceExecutionHostId)
|
||||
const activeWorkspaceKey = useAppStore((s) => s.activeWorkspaceKey)
|
||||
@@ -92,7 +96,7 @@ const WorktreeList = React.memo(function WorktreeList({
|
||||
const collapsedGroups = useAppStore((s) => s.collapsedGroups)
|
||||
const toggleGroup = useAppStore((s) => s.toggleCollapsedGroup)
|
||||
const projectGroups = useAppStore((s) => s.projectGroups ?? EMPTY_PROJECT_GROUPS)
|
||||
const folderWorkspaces = useAppStore((s) => s.folderWorkspaces)
|
||||
const folderWorkspaces = useAppStore((s) => selectFolderWorkspaces(s))
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const cardProps = useAppStore((s) => s.worktreeCardProperties)
|
||||
const { prCache, hostedReviewCache } = useAppStore(
|
||||
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
} from './worktree-visibility-use-global'
|
||||
import { createWorktreeVisibilitySourceMutation } from './worktree-visibility-source-mutation'
|
||||
import { createBrowserUuid } from '@/lib/browser-uuid'
|
||||
import { selectDetectedWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export default function WorktreeVisibilityDialog(): React.JSX.Element | null {
|
||||
const activeModal = useAppStore((s) => s.activeModal)
|
||||
@@ -65,7 +66,7 @@ export default function WorktreeVisibilityDialog(): React.JSX.Element | null {
|
||||
const repos = useAppStore((s) => s.repos)
|
||||
const updateRepo = useAppStore((s) => s.updateRepo)
|
||||
const fetchWorktrees = useAppStore((s) => s.fetchWorktrees)
|
||||
const detectedWorktreesByRepo = useAppStore((s) => s.detectedWorktreesByRepo)
|
||||
const detectedWorktreesByRepo = useAppStore((s) => selectDetectedWorktreesByRepo(s))
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const [actionState, setActionState] = useState<NewExternalWorktreesInboxActionState | null>(null)
|
||||
const [busyPath, setBusyPath] = useState<string | null>(null)
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
getHostScopedWorktreeLineageInputs,
|
||||
getWorktreeLineageAncestors
|
||||
} from '../../../../shared/sidebar-list/worktree-lineage-projection'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
type ChildWorkspacesToggleState = Pick<
|
||||
AppState,
|
||||
@@ -91,7 +92,7 @@ export function getRenderedLineageChipKeys(state: AppState): Set<string> {
|
||||
)
|
||||
}
|
||||
const visibleWorktrees = computeVisibleWorktrees(
|
||||
state.worktreesByRepo,
|
||||
selectWorktreesByRepo(state),
|
||||
[],
|
||||
buildVisibleWorktreeOptionsFromState(state, getRepoMapFromState(state))
|
||||
)
|
||||
|
||||
@@ -10,6 +10,10 @@ import { relativePathInsideRoot } from '../../../../shared/cross-platform-path'
|
||||
import { markOnboardingProjectAdded } from '@/lib/onboarding-project-checklist'
|
||||
import { finalizeImportedRepoAfterSkip } from './add-repo-skip-finalization'
|
||||
import { parseExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectWorktreesByRepo
|
||||
} from '@/store/workspace-record-selectors'
|
||||
|
||||
type DefaultCheckoutHandoffReason = EventProps<'add_repo_default_checkout_handoff'>['reason']
|
||||
|
||||
@@ -91,7 +95,7 @@ async function revealDetectedHiddenLinkedExternalWorktrees(
|
||||
const state = useAppStore.getState()
|
||||
if (
|
||||
!hasDetectedHiddenLinkedExternalWorktrees(
|
||||
state.detectedWorktreesByRepo[repoId],
|
||||
selectDetectedWorktreesByRepo(state)[repoId],
|
||||
executionHostId
|
||||
)
|
||||
) {
|
||||
@@ -124,7 +128,7 @@ async function findDetectedDefaultCheckout(
|
||||
reason: DefaultCheckoutHandoffReason
|
||||
}> {
|
||||
const state = useAppStore.getState()
|
||||
const detected = state.detectedWorktreesByRepo[repoId]
|
||||
const detected = selectDetectedWorktreesByRepo(state)[repoId]
|
||||
const detectedDefaultCheckout = getDetectedProjectDefaultCheckout(detected, executionHostId)
|
||||
if (!detectedDefaultCheckout) {
|
||||
return {
|
||||
@@ -155,7 +159,7 @@ async function findDetectedDefaultCheckout(
|
||||
}
|
||||
const worktree = getProjectDefaultCheckout(
|
||||
getProjectWorktreesForHost(
|
||||
useAppStore.getState().worktreesByRepo[repoId] ?? [],
|
||||
selectWorktreesByRepo(useAppStore.getState())[repoId] ?? [],
|
||||
executionHostId
|
||||
)
|
||||
)
|
||||
@@ -191,7 +195,7 @@ export async function openProjectDefaultCheckout({
|
||||
}): Promise<void> {
|
||||
let defaultCheckout = getProjectDefaultCheckout(
|
||||
getProjectWorktreesForHost(
|
||||
useAppStore.getState().worktreesByRepo[repoId] ?? [],
|
||||
selectWorktreesByRepo(useAppStore.getState())[repoId] ?? [],
|
||||
executionHostId
|
||||
)
|
||||
)
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
filterProjectGroupsForVisibleHosts,
|
||||
getVisibleSidebarHostIdSet
|
||||
} from '../../../../shared/sidebar-list/host-filtering'
|
||||
import { selectFolderWorkspaces } from '@/store/workspace-record-selectors'
|
||||
|
||||
const EMPTY_REPO_ID_SET: ReadonlySet<string> = Object.freeze(new Set<string>())
|
||||
const EMPTY_IMPORTED_BY_REPO: ReadonlyMap<string, ImportedWorktreesCardCandidate> = new Map()
|
||||
@@ -98,7 +99,7 @@ export function computeRenderedSidebarRows(
|
||||
pendingCreations: EMPTY_PENDING_CREATIONS,
|
||||
projectGrouping: { projects: projection.projects, projectHostSetups: projection.setups },
|
||||
folderWorkspaces: filterFolderWorkspacesForVisibleHosts(
|
||||
state.folderWorkspaces,
|
||||
selectFolderWorkspaces(state),
|
||||
projectGroups,
|
||||
visibleHostIdSet,
|
||||
defaultHostId
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { compareWorktreeDisplayName } from '@/lib/worktree-display-name-order'
|
||||
import { finishProjectAddWithDefaultCheckout } from './project-added-default-checkout'
|
||||
import type { ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
type CompleteGitRepoAddOptions = {
|
||||
closeModal: () => void
|
||||
@@ -40,7 +41,7 @@ export function useCompleteGitRepoAdd({
|
||||
source: AddRepoExistingWorkspaceSource,
|
||||
executionHostId?: ExecutionHostId
|
||||
): Promise<void> => {
|
||||
const worktrees = (useAppStore.getState().worktreesByRepo[repoId] ?? []).filter(
|
||||
const worktrees = (selectWorktreesByRepo(useAppStore.getState())[repoId] ?? []).filter(
|
||||
(worktree) =>
|
||||
executionHostId === undefined ||
|
||||
worktree.hostId === executionHostId ||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '../../../../shared/workspace-creator-visibility'
|
||||
import { getStructuredChatWorktreeIds } from './visible-worktree-activity-inputs'
|
||||
import { getWorktreeHostIdentity } from '../../../../shared/worktree/host-qualified-identity'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
type UseVisibleWorkspaceKanbanWorktreeIdsParams = {
|
||||
allWorktrees: readonly Worktree[]
|
||||
@@ -27,7 +28,7 @@ export function useVisibleWorkspaceKanbanWorktreeIds({
|
||||
allWorktrees,
|
||||
repoMap
|
||||
}: UseVisibleWorkspaceKanbanWorktreeIdsParams): ReadonlySet<string> {
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const worktreesByRepo = useAppStore((s) => selectWorktreesByRepo(s))
|
||||
const showSleepingWorkspaces = useAppStore((s) => s.showSleepingWorkspaces)
|
||||
const hideDefaultBranchWorkspace = useAppStore((s) => s.hideDefaultBranchWorkspace)
|
||||
const hideAutomationGeneratedWorkspaces = useAppStore((s) => s.hideAutomationGeneratedWorkspaces)
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import type { Worktree } from '../../../../shared/worktree/types'
|
||||
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { folderWorkspaceToWorktree } from '../../../../shared/folder-workspace-worktree'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
/** Resolves the edited workspace within its execution host, including folders. */
|
||||
export function useWorktreeMetaWorkspace(args: {
|
||||
@@ -27,10 +28,10 @@ export function useWorktreeMetaWorkspace(args: {
|
||||
// reports as ambiguous rather than guessing. The row that opened the dialog
|
||||
// knows which one it is; the index stays the fallback for callers that cannot.
|
||||
const owner = executionHostId
|
||||
? findIndexedWorktreeOwnerForHost(s.worktreesByRepo, worktreeId, executionHostId)
|
||||
: findIndexedWorktreeOwner(s.worktreesByRepo, worktreeId)
|
||||
? findIndexedWorktreeOwnerForHost(selectWorktreesByRepo(s), worktreeId, executionHostId)
|
||||
: findIndexedWorktreeOwner(selectWorktreesByRepo(s), worktreeId)
|
||||
return owner && (!ownerRepoId || owner.repoId === ownerRepoId)
|
||||
? s.worktreesByRepo[owner.repoId]?.find((item) => item === owner)
|
||||
? selectWorktreesByRepo(s)[owner.repoId]?.find((item) => item === owner)
|
||||
: undefined
|
||||
})
|
||||
// Why: folder workspaces are absent from worktreesByRepo, so the lookup above
|
||||
@@ -42,7 +43,7 @@ export function useWorktreeMetaWorkspace(args: {
|
||||
if (workspaceScope?.type !== 'folder') {
|
||||
return null
|
||||
}
|
||||
const candidates = s.folderWorkspaces.filter(
|
||||
const candidates = selectFolderWorkspaces(s).filter(
|
||||
(item) =>
|
||||
item.id === workspaceScope.folderWorkspaceId &&
|
||||
(!executionHostId || getFolderWorkspaceHostId(item, s.projectGroups) === executionHostId)
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { Repo } from '../../../../shared/repo-types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { extractIpcErrorMessage } from '@/lib/ipc-error'
|
||||
import { upsertAddedRepoWithProjectHostSetup } from './add-repo-store-upsert'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
import { resolveAddRepoRuntimeTarget, worktreeRefreshOptions } from './add-repo-runtime-owner'
|
||||
import type { ExecutionHostId } from '../../../../shared/execution-host'
|
||||
|
||||
@@ -170,13 +171,11 @@ export function useCreateRepo(
|
||||
if (!isCurrent()) {
|
||||
return
|
||||
}
|
||||
const folderWorktree = useAppStore
|
||||
.getState()
|
||||
.worktreesByRepo[repo.id]?.find(
|
||||
(worktree) =>
|
||||
ownerOptions.executionHostId === undefined ||
|
||||
worktree.hostId === ownerOptions.executionHostId
|
||||
)
|
||||
const folderWorktree = selectWorktreesByRepo(useAppStore.getState())[repo.id]?.find(
|
||||
(worktree) =>
|
||||
ownerOptions.executionHostId === undefined ||
|
||||
worktree.hostId === ownerOptions.executionHostId
|
||||
)
|
||||
if (folderWorktree) {
|
||||
activateAndRevealWorktree(folderWorktree.id, {
|
||||
sidebarRevealBehavior: 'auto',
|
||||
|
||||
@@ -20,6 +20,7 @@ export {
|
||||
getWorktreeIdsWithStructuredChat
|
||||
} from './visible-worktree-activity-inputs'
|
||||
import { buildVisibleWorktreeOptionsFromState } from './visible-worktree-options-from-state'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
import { useAppStore } from '@/store'
|
||||
import { getAllWorktreesFromState, getRepoMapFromState } from '@/store/selectors'
|
||||
import { getSettingsFocusedExecutionHostId } from '../../../../shared/execution-host'
|
||||
@@ -101,7 +102,7 @@ export function getVisibleWorktreeIds(): string[] {
|
||||
}
|
||||
|
||||
const visibleIds = computeVisibleWorktreeIds(
|
||||
state.worktreesByRepo,
|
||||
selectWorktreesByRepo(state),
|
||||
sortedIds,
|
||||
buildVisibleWorktreeOptionsFromState(state, repoMap)
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '../../../../shared/execution-host'
|
||||
import { computeVisibleWorktrees } from './visible-worktrees'
|
||||
import { buildVisibleWorktreeOptionsFromState } from './visible-worktree-options-from-state'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
/**
|
||||
* Filter-only visibility for one worktree id: runs the sidebar filter pipeline
|
||||
@@ -28,7 +29,7 @@ export function worktreePassesSidebarFilters(
|
||||
const requestedHostId = executionHostId ? normalizeExecutionHostId(executionHostId) : null
|
||||
const defaultHostId = getSettingsFocusedExecutionHostId(state.settings)
|
||||
return computeVisibleWorktrees(
|
||||
state.worktreesByRepo,
|
||||
selectWorktreesByRepo(state),
|
||||
[],
|
||||
buildVisibleWorktreeOptionsFromState(state, repoMap)
|
||||
).some((worktree) => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { AppState } from '../../../../store/types'
|
||||
import type { WorktreeMetaBatchUpdate } from '../../../../store/slices/worktree-helpers'
|
||||
import { getAllWorktreesFromState } from '../../../../store/selectors'
|
||||
import { getWorkspaceStatus } from '../../workspace-status'
|
||||
import { selectFolderWorkspaces } from '@/store/workspace-record-selectors'
|
||||
|
||||
/** The cards a drag picked up, by host: a twin on another host shares each card's id. */
|
||||
export type DraggedWorktreeCopies = readonly Pick<Worktree, 'id' | 'hostId'>[]
|
||||
@@ -42,7 +43,7 @@ export function dropWriteRows(
|
||||
// Folder workspaces share the manual-order catalog, so a drop ranks them too.
|
||||
for (const row of [
|
||||
...getAllWorktreesFromState(state),
|
||||
...state.folderWorkspaces.map(folderWorkspaceToWorktree)
|
||||
...selectFolderWorkspaces(state).map(folderWorkspaceToWorktree)
|
||||
]) {
|
||||
rowsById.set(row.id, [...(rowsById.get(row.id) ?? []), row])
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AppState } from '@/store/types'
|
||||
import type { WorktreeCardProperty } from '../../../../../../shared/ui-chrome-types'
|
||||
import type { WorktreeGroupBy } from '../../../../../../shared/sidebar-list/row-types'
|
||||
import { selectFolderWorkspaces } from '@/store/workspace-record-selectors'
|
||||
|
||||
export type WorktreeListReviewCacheState = Pick<
|
||||
AppState,
|
||||
@@ -24,7 +25,7 @@ export function selectWorktreeListReviewCacheInputs(
|
||||
groupBy: WorktreeGroupBy,
|
||||
cardProperties: readonly WorktreeCardProperty[]
|
||||
): WorktreeListReviewCacheInputs {
|
||||
const hasFolderWorkspaces = state.folderWorkspaces.length > 0
|
||||
const hasFolderWorkspaces = selectFolderWorkspaces(state).length > 0
|
||||
const newCardStyle = state.settings?.experimentalNewWorktreeCardStyle === true
|
||||
const folderCardsNeedReview =
|
||||
hasFolderWorkspaces &&
|
||||
|
||||
+2
-1
@@ -17,6 +17,7 @@ import {
|
||||
type NewExternalWorktreesInboxActionState
|
||||
} from '../../new-external-worktrees-inbox-actions'
|
||||
import { buildNewExternalWorktreesInboxCandidates } from '../../new-external-worktrees-inbox-candidates'
|
||||
import { selectDetectedWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
// The two sidebar notice rows for worktrees Orca detected but does not yet show, plus the
|
||||
// pending/error state their inline actions surface.
|
||||
@@ -123,7 +124,7 @@ export function useSidebarExternalWorktreeCards(args: {
|
||||
})
|
||||
return
|
||||
}
|
||||
detected = useAppStore.getState().detectedWorktreesByRepo[projectId]
|
||||
detected = selectDetectedWorktreesByRepo(useAppStore.getState())[projectId]
|
||||
}
|
||||
if (detected?.authoritative !== true) {
|
||||
setImportedWorktreeCardState(projectId, {
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
getVisibleWorktreeTerminalActivityTabs,
|
||||
getWorktreeIdsWithStructuredChat
|
||||
} from '../../visible-worktree-activity-inputs'
|
||||
import { selectFolderWorkspaces } from '@/store/workspace-record-selectors'
|
||||
|
||||
export type SidebarWorktreeFilters = ReturnType<typeof useSidebarWorktreeFilters>
|
||||
|
||||
@@ -131,7 +132,7 @@ function getRevealTargetHostId(state: AppState, worktree: Worktree, repo: Repo |
|
||||
const defaultHostId = getSettingsFocusedExecutionHostId(state.settings)
|
||||
const workspaceScope = parseWorkspaceKey(worktree.id)
|
||||
if (workspaceScope?.type === 'folder') {
|
||||
const folderWorkspace = state.folderWorkspaces.find(
|
||||
const folderWorkspace = selectFolderWorkspaces(state).find(
|
||||
(candidate) => candidate.id === workspaceScope.folderWorkspaceId
|
||||
)
|
||||
if (folderWorkspace) {
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
import type { ExecutionHostRegistryEntry } from '../../../../../../shared/execution-host-registry'
|
||||
import { buildSidebarHostOptions } from '../../sidebar-host-options'
|
||||
import { selectPendingWorktreeCreationKeys } from './pending-worktree-creation-keys'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
type SectionRowsArgs = {
|
||||
agentSendTargetWorktreeId?: string | null
|
||||
@@ -70,7 +71,7 @@ type SectionRowsArgs = {
|
||||
// tier wrapped around them.
|
||||
export function useSidebarSectionRows(args: SectionRowsArgs) {
|
||||
const { repos, worktrees, repoMap, defaultHostId } = args
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const worktreesByRepo = useAppStore((s) => selectWorktreesByRepo(s))
|
||||
const sshTargetLabels = useAppStore((s) => s.sshTargetLabels)
|
||||
const sshConnectionStates = useAppStore((s) => s.sshConnectionStates)
|
||||
const runtimeEnvironments = useAppStore((s) => s.runtimeEnvironments)
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useReusedArrayIdentity } from './use-reused-array-identity'
|
||||
import { useSidebarListSession } from '@/lib/sidebar-list-session'
|
||||
import { ORCA_SIDEBAR_INPUTS_IS_MAIN } from '../../../../../../shared/sidebar-inputs-switch'
|
||||
import { getWorktreeHostIdentity } from '../../../../../../shared/worktree/host-qualified-identity'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
// ── Stable sort order ──────────────────────────────────────────
|
||||
// Why sortEpoch (not selection): selection side-effects (clearing isUnread, PR-cache refresh) must not reorder the sidebar under the user.
|
||||
@@ -93,7 +94,7 @@ function useStampSidebarWorktreeSortOrder(args: {
|
||||
sortBy: SortBy
|
||||
}): string[] {
|
||||
const { repoMap, sortBy } = args
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const worktreesByRepo = useAppStore((s) => selectWorktreesByRepo(s))
|
||||
const ranked = useMemo(
|
||||
() =>
|
||||
rankWorktreesForSort({
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { SortBy } from '../../../../../../shared/sidebar-list/smart-sort'
|
||||
import type { SidebarWorktreeFilters } from './use-filters'
|
||||
import { useReusedArrayIdentity } from './use-reused-array-identity'
|
||||
import { useSleepingSweepRetention } from './use-sleeping-sweep-retention'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
const EMPTY_WORKTREE_ID_SET: ReadonlySet<string> = new Set()
|
||||
|
||||
@@ -49,7 +50,7 @@ export function useVisibleSidebarWorktrees(args: {
|
||||
visibleWorkspaceHostIds,
|
||||
workspaceHostScope
|
||||
} = filterState
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const worktreesByRepo = useAppStore((s) => selectWorktreesByRepo(s))
|
||||
const agentStatusEpoch = useAppStore((s) => (!showSleepingWorkspaces ? s.agentStatusEpoch : 0))
|
||||
// Why: skip the clock entirely when the epoch is the opt-out sentinel, so a
|
||||
// sleeping-workspaces list cannot evict the sample the live lists share.
|
||||
|
||||
+2
-1
@@ -11,13 +11,14 @@ import {
|
||||
handleRepoHeaderActionPointerDown,
|
||||
stopRepoHeaderKeyboardToggle
|
||||
} from './header-event-guards'
|
||||
import { selectDetectedWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
/**
|
||||
* Marks a repo whose worktree scan failed, so its rows are retained but cannot be trusted.
|
||||
* Click re-runs the scan: the failure is otherwise re-tried only by the next incidental refresh.
|
||||
*/
|
||||
export function RepoScanUnavailableIndicator({ repo }: { repo: Repo }): React.JSX.Element | null {
|
||||
const detected = useAppStore((s) => s.detectedWorktreesByRepo[repo.id])
|
||||
const detected = useAppStore((s) => selectDetectedWorktreesByRepo(s)[repo.id])
|
||||
const fetchWorktrees = useAppStore((s) => s.fetchWorktrees)
|
||||
const [pending, setPending] = React.useState(false)
|
||||
const failure = resolveRepoScanFailure(repo, detected)
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
parseExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../../../shared/execution-host'
|
||||
import { selectDetectedWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export function resolveWorktreeVisibilityHostTarget(
|
||||
state: Pick<AppState, 'repos' | 'settings' | 'detectedWorktreesByRepo'>,
|
||||
@@ -18,7 +19,7 @@ export function resolveWorktreeVisibilityHostTarget(
|
||||
hostId: requestedHostId,
|
||||
settings: state.settings
|
||||
})
|
||||
const detectedForRepo = repoId ? state.detectedWorktreesByRepo[repoId] : undefined
|
||||
const detectedForRepo = repoId ? selectDetectedWorktreesByRepo(state)[repoId] : undefined
|
||||
const detected =
|
||||
detectedForRepo && repo && requestedHostId
|
||||
? {
|
||||
|
||||
@@ -25,6 +25,7 @@ import { checklistItemsFromVersion } from './skill-package-checklist-items'
|
||||
import { summarizeSkillInstallRisk } from './skill-package-install-risk'
|
||||
import { retryableSkillIds } from './skill-bundle-retry-selection'
|
||||
import { createBrowserUuid } from '@/lib/browser-uuid'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
type BundleVersion = SkillCloudVersion & {
|
||||
manifest: Extract<SkillCloudVersion['manifest'], { skills: unknown }>
|
||||
@@ -47,9 +48,9 @@ export function SkillBundleInstallFlow(props: {
|
||||
}): React.JSX.Element {
|
||||
const runtimeEnvironments = useAppStore((state) => state.runtimeEnvironments)
|
||||
const runtimeStatus = useAppStore((state) => state.runtimeStatusByEnvironmentId)
|
||||
const worktreesByRepo = useAppStore((state) => state.worktreesByRepo)
|
||||
const worktreesByRepo = useAppStore((state) => selectWorktreesByRepo(state))
|
||||
const repos = useAppStore((state) => state.repos)
|
||||
const folderWorkspaces = useAppStore((state) => state.folderWorkspaces)
|
||||
const folderWorkspaces = useAppStore((state) => selectFolderWorkspaces(state))
|
||||
const sshConnectionStates = useAppStore((state) => state.sshConnectionStates)
|
||||
const sshTargetLabels = useAppStore((state) => state.sshTargetLabels)
|
||||
const allSkillIds = useMemo(
|
||||
|
||||
@@ -27,6 +27,7 @@ import { resolveSkillShareForInstall } from './skill-warning-preview-gate'
|
||||
import { useSkillInstallRisk } from './use-skill-install-risk'
|
||||
import { SkillInstallDialogFooter } from './SkillInstallDialogFooter'
|
||||
import { createBrowserUuid } from '@/lib/browser-uuid'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export function SkillInstallDialog({
|
||||
open,
|
||||
@@ -39,9 +40,9 @@ export function SkillInstallDialog({
|
||||
}): React.JSX.Element {
|
||||
const runtimeEnvironments = useAppStore((state) => state.runtimeEnvironments)
|
||||
const runtimeStatus = useAppStore((state) => state.runtimeStatusByEnvironmentId)
|
||||
const worktreesByRepo = useAppStore((state) => state.worktreesByRepo)
|
||||
const worktreesByRepo = useAppStore((state) => selectWorktreesByRepo(state))
|
||||
const repos = useAppStore((state) => state.repos)
|
||||
const folderWorkspaces = useAppStore((state) => state.folderWorkspaces)
|
||||
const folderWorkspaces = useAppStore((state) => selectFolderWorkspaces(state))
|
||||
const sshConnectionStates = useAppStore((state) => state.sshConnectionStates)
|
||||
const sshTargetLabels = useAppStore((state) => state.sshTargetLabels)
|
||||
const [link, setLink] = useState(initialLink)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AppState } from '../../store'
|
||||
import { getAllWorktreesFromState } from '../../store/selectors'
|
||||
import { selectFolderWorkspaces } from '@/store/workspace-record-selectors'
|
||||
|
||||
const EMPTY_TABS_BY_WORKTREE: AppState['tabsByWorktree'] = {}
|
||||
const EMPTY_PTY_IDS_BY_TAB_ID: AppState['ptyIdsByTabId'] = {}
|
||||
@@ -72,7 +73,7 @@ export function getResourceUsageFolderWorkspaces(
|
||||
state: Pick<AppState, 'folderWorkspaces'>,
|
||||
open: boolean
|
||||
): AppState['folderWorkspaces'] {
|
||||
return open ? state.folderWorkspaces : EMPTY_FOLDER_WORKSPACES
|
||||
return open ? selectFolderWorkspaces(state) : EMPTY_FOLDER_WORKSPACES
|
||||
}
|
||||
|
||||
export function getResourceUsageProjectGroups(
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
WorkspaceSpaceSortKey
|
||||
} from './workspace-space-presentation'
|
||||
import type { WorkspaceGitRefreshState } from './workspace-space-manager-state-types'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export function useWorkspaceSpaceManagerBindings() {
|
||||
const analysis = useAppStore((state) => state.workspaceSpaceAnalysis)
|
||||
@@ -21,7 +22,7 @@ export function useWorkspaceSpaceManagerBindings() {
|
||||
const repoMap = useAppStore((state) => getRepoMapFromState(state))
|
||||
const repos = useAppStore((state) => state.repos)
|
||||
const worktreeMap = useAppStore((state) => getWorktreeMapFromState(state))
|
||||
const worktreesByRepo = useAppStore((state) => state.worktreesByRepo)
|
||||
const worktreesByRepo = useAppStore((state) => selectWorktreesByRepo(state))
|
||||
const tabsByWorktree = useAppStore((state) => state.tabsByWorktree)
|
||||
const ptyIdsByTabId = useAppStore((state) => state.ptyIdsByTabId)
|
||||
const agentStatusByPaneKey = useAppStore((state) => state.agentStatusByPaneKey)
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type SearchableWorkspaceTab
|
||||
} from '@/lib/workspace-tab-palette-search'
|
||||
import type { AppState } from '@/store/types'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
import {
|
||||
getRepoExecutionHostId,
|
||||
getWorktreeExecutionHostId,
|
||||
@@ -101,7 +102,7 @@ export function selectOpenTabSearchEntryState(
|
||||
browserPagesByWorkspace: state.browserPagesByWorkspace,
|
||||
browserTabsByWorktree: state.browserTabsByWorktree,
|
||||
executionHostId,
|
||||
folderWorkspaces: state.folderWorkspaces,
|
||||
folderWorkspaces: selectFolderWorkspaces(state),
|
||||
generatedTitlesEnabled: state.settings?.tabAutoGenerateTitle === true,
|
||||
groupsByWorktree: state.groupsByWorktree,
|
||||
openFiles: state.openFiles,
|
||||
@@ -109,7 +110,7 @@ export function selectOpenTabSearchEntryState(
|
||||
tabsByWorktree: state.tabsByWorktree,
|
||||
unifiedTabsByWorktree: state.unifiedTabsByWorktree,
|
||||
worktree,
|
||||
worktreesByRepo: state.worktreesByRepo
|
||||
worktreesByRepo: selectWorktreesByRepo(state)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@ import type { RuntimeFileOperationArgs } from '@/runtime/runtime-file-client-typ
|
||||
import type { useAppStore } from '@/store'
|
||||
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { runtimeTargetEnvironmentId } from '@/runtime/runtime-client-target'
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectFolderWorkspaces,
|
||||
selectWorktreesByRepo
|
||||
} from '@/store/workspace-record-selectors'
|
||||
|
||||
type TabEntryAbsolutePathOwnerState = Pick<
|
||||
ReturnType<typeof useAppStore.getState>,
|
||||
@@ -92,9 +97,9 @@ export function createTabEntryAllowAbsolutePathsSelector(
|
||||
if (
|
||||
previousSlices?.settings === state.settings &&
|
||||
previousSlices.repos === state.repos &&
|
||||
previousSlices.worktreesByRepo === state.worktreesByRepo &&
|
||||
previousSlices.detectedWorktreesByRepo === state.detectedWorktreesByRepo &&
|
||||
previousSlices.folderWorkspaces === state.folderWorkspaces &&
|
||||
selectWorktreesByRepo(previousSlices) === selectWorktreesByRepo(state) &&
|
||||
selectDetectedWorktreesByRepo(previousSlices) === selectDetectedWorktreesByRepo(state) &&
|
||||
selectFolderWorkspaces(previousSlices) === selectFolderWorkspaces(state) &&
|
||||
previousSlices.projectGroups === state.projectGroups &&
|
||||
previousSlices.runtimeEnvironments === state.runtimeEnvironments &&
|
||||
previousSlices.runtimeEnvironmentCatalogHydrated ===
|
||||
@@ -110,9 +115,9 @@ export function createTabEntryAllowAbsolutePathsSelector(
|
||||
previousSlices = {
|
||||
settings: state.settings,
|
||||
repos: state.repos,
|
||||
worktreesByRepo: state.worktreesByRepo,
|
||||
detectedWorktreesByRepo: state.detectedWorktreesByRepo,
|
||||
folderWorkspaces: state.folderWorkspaces,
|
||||
worktreesByRepo: selectWorktreesByRepo(state),
|
||||
detectedWorktreesByRepo: selectDetectedWorktreesByRepo(state),
|
||||
folderWorkspaces: selectFolderWorkspaces(state),
|
||||
projectGroups: state.projectGroups,
|
||||
runtimeEnvironments: state.runtimeEnvironments,
|
||||
runtimeEnvironmentCatalogHydrated: state.runtimeEnvironmentCatalogHydrated,
|
||||
|
||||
@@ -42,6 +42,7 @@ import { resolveAiVaultHostScopeDefaults } from '../right-sidebar/ai-vault-host-
|
||||
import { getAiVaultResumeWorkspaceExecutionHostId } from '@/lib/ai-vault-resume-target'
|
||||
import { claimAiVaultForcedRescan } from '../right-sidebar/ai-vault-session-refresh'
|
||||
import { structuredAgentSessionLaunchFeasible } from '@/lib/agent-session-launch-plan'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
import { workspaceKindForWorktreeId } from '../../../../shared/workspace-launch-kind'
|
||||
|
||||
/** What a tab's history row is found by: a native chat tab by the chat it shows, a terminal tab by
|
||||
@@ -193,8 +194,8 @@ export function resolveTabSessionSwitch(
|
||||
subject: TabSessionHistorySubject
|
||||
): TabSessionSwitch | null {
|
||||
const worktrees = aiVaultSessionOwnerWorkspaces(
|
||||
getIndexedAllWorktrees(state.worktreesByRepo),
|
||||
state.folderWorkspaces
|
||||
getIndexedAllWorktrees(selectWorktreesByRepo(state)),
|
||||
selectFolderWorkspaces(state)
|
||||
)
|
||||
const { resumeState, resumeInChat } = resolveAiVaultHistoryRowResume({
|
||||
session,
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import { shouldShowWindowsShellMenu } from './windows-shell-menu-visibility'
|
||||
import { createUnifiedTabLookup } from './tab-bar-item-model'
|
||||
import { getClientCreationActionPolicy } from '@/lib/client-creation-action-policy'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
const isWindows = navigator.userAgent.includes('Windows')
|
||||
export const isMacOs = navigator.userAgent.includes('Mac')
|
||||
@@ -174,7 +175,7 @@ export function useTabBarRuntimeModel({
|
||||
const projects = useAppStore((s) => (needsLocalProjectRuntime ? s.projects : EMPTY_PROJECTS))
|
||||
const repos = useAppStore((s) => (needsLocalProjectRuntime ? s.repos : EMPTY_REPOS))
|
||||
const worktreesByRepo = useAppStore((s) =>
|
||||
needsLocalProjectRuntime ? s.worktreesByRepo : EMPTY_WORKTREES_BY_REPO
|
||||
needsLocalProjectRuntime ? selectWorktreesByRepo(s) : EMPTY_WORKTREES_BY_REPO
|
||||
)
|
||||
const localProjectRuntime = useMemo(() => {
|
||||
if (!needsLocalProjectRuntime) {
|
||||
|
||||
@@ -2,11 +2,12 @@ import type { AppState } from '@/store'
|
||||
import { getWorktreeMapFromState } from '@/store/selectors'
|
||||
import { makePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { selectFolderWorkspaces } from '@/store/workspace-record-selectors'
|
||||
|
||||
export function getWorkspacePath(state: AppState, worktreeId: string): string | null {
|
||||
const parsed = parseWorkspaceKey(worktreeId)
|
||||
return parsed?.type === 'folder'
|
||||
? (state.folderWorkspaces.find((workspace) => workspace.id === parsed.folderWorkspaceId)
|
||||
? (selectFolderWorkspaces(state).find((workspace) => workspace.id === parsed.folderWorkspaceId)
|
||||
?.folderPath ?? null)
|
||||
: (getWorktreeMapFromState(state).get(worktreeId)?.path ?? null)
|
||||
}
|
||||
@@ -20,7 +21,7 @@ export function buildPaneIdentityEnv(
|
||||
const parsed = parseWorkspaceKey(worktreeId)
|
||||
const folderWorkspace =
|
||||
parsed?.type === 'folder'
|
||||
? state.folderWorkspaces.find((workspace) => workspace.id === parsed.folderWorkspaceId)
|
||||
? selectFolderWorkspaces(state).find((workspace) => workspace.id === parsed.folderWorkspaceId)
|
||||
: null
|
||||
return {
|
||||
ORCA_WORKSPACE_ID: worktreeId,
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { Worktree } from '../../../../shared/worktree/types'
|
||||
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { parsePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import type { TerminalPaneLayoutNode } from '../../../../shared/terminal-tab-types'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
type StoreSnapshot = ReturnType<typeof useAppStore.getState>
|
||||
|
||||
@@ -172,7 +173,7 @@ function findWorktreeRowOnItsOwnHost(
|
||||
state: StoreSnapshot,
|
||||
worktreeId: string
|
||||
): { worktree: Worktree | undefined; hostId: ExecutionHostId | null } {
|
||||
const rows = getIndexedWorktreesById(state.worktreesByRepo, worktreeId)
|
||||
const rows = getIndexedWorktreesById(selectWorktreesByRepo(state), worktreeId)
|
||||
if (rows.length <= 1) {
|
||||
return { worktree: rows[0], hostId: null }
|
||||
}
|
||||
@@ -218,7 +219,10 @@ export function getNotificationWorkspaceLabels(
|
||||
const scope = parseWorkspaceKey(workspaceId)
|
||||
const fallback = terminalTitle?.trim() || 'workspace'
|
||||
if (scope?.type === 'folder') {
|
||||
const folder = findIndexedFolderWorkspaceOwner(state.folderWorkspaces, scope.folderWorkspaceId)
|
||||
const folder = findIndexedFolderWorkspaceOwner(
|
||||
selectFolderWorkspaces(state),
|
||||
scope.folderWorkspaceId
|
||||
)
|
||||
// The group ID is only unique per host, so qualify it with the folder's own host.
|
||||
const group =
|
||||
folder &&
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useAppStore } from '@/store'
|
||||
import { getFileExplorerOperationOwnerFromState } from './right-sidebar/file-explorer-operation-owner'
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectFolderWorkspaces,
|
||||
selectWorktreesByRepo
|
||||
} from '@/store/workspace-record-selectors'
|
||||
|
||||
export function useQuickOpenInteraction(worktreeId: string | null): {
|
||||
opening: boolean
|
||||
@@ -25,11 +30,11 @@ export function useQuickOpenInteraction(worktreeId: string | null): {
|
||||
state.activeWorkspaceExecutionHostId !== previous.activeWorkspaceExecutionHostId ||
|
||||
((state.settings !== previous.settings ||
|
||||
state.repos !== previous.repos ||
|
||||
state.worktreesByRepo !== previous.worktreesByRepo ||
|
||||
state.detectedWorktreesByRepo !== previous.detectedWorktreesByRepo ||
|
||||
selectWorktreesByRepo(state) !== selectWorktreesByRepo(previous) ||
|
||||
selectDetectedWorktreesByRepo(state) !== selectDetectedWorktreesByRepo(previous) ||
|
||||
state.restoredRuntimeHostIdByWorkspaceSessionKey !==
|
||||
previous.restoredRuntimeHostIdByWorkspaceSessionKey ||
|
||||
state.folderWorkspaces !== previous.folderWorkspaces ||
|
||||
selectFolderWorkspaces(state) !== selectFolderWorkspaces(previous) ||
|
||||
state.projectGroups !== previous.projectGroups) &&
|
||||
JSON.stringify(getFileExplorerOperationOwnerFromState(state, worktreeId)) !==
|
||||
JSON.stringify(getFileExplorerOperationOwnerFromState(previous, worktreeId)))
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
} from '@/lib/provider-runtime-context'
|
||||
import { useRef } from 'react'
|
||||
import { getScreenSubmitShortcutLabel } from '@/lib/screen-submit-shortcut'
|
||||
import { selectFolderWorkspaces } from '@/store/workspace-record-selectors'
|
||||
|
||||
export function useTaskPageStoreBindings() {
|
||||
useTranslation()
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
@@ -43,7 +45,7 @@ export function useTaskPageStoreBindings() {
|
||||
const searchLinearIssues = useAppStore((s) => s.searchLinearIssues)
|
||||
const listLinearIssues = useAppStore((s) => s.listLinearIssues)
|
||||
const linearListInvalidationToken = useAppStore((s) => s.linearListInvalidationToken)
|
||||
const folderWorkspaces = useAppStore((s) => s.folderWorkspaces)
|
||||
const folderWorkspaces = useAppStore((s) => selectFolderWorkspaces(s))
|
||||
const invalidateLinearIssueLists = useAppStore((s) => s.invalidateLinearIssueLists)
|
||||
const getCachedLinearIssues = useAppStore((s) => s.getCachedLinearIssues)
|
||||
const fetchLinearIssue = useAppStore((s) => s.fetchLinearIssue)
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useReusedArrayIdentity } from './sidebar/worktree-list/listing/use-reus
|
||||
import { selectPairedRuntimeParkingEnvironmentIds } from './terminal-pane/terminal-hidden-view-parking'
|
||||
import { createTerminalWorktreeTopologyProjection } from './terminal-pane/terminal-hidden-worktree-retention'
|
||||
import { isMainTerminalSideEffectAuthorityForPty } from './terminal-pane/terminal-side-effect-facts-handler'
|
||||
import { selectFolderWorkspaces } from '@/store/workspace-record-selectors'
|
||||
|
||||
export function useTerminalWorkspaceFoundation() {
|
||||
const terminalTopologyProjectionRef = useRef<WorktreeTabBucketProjection<
|
||||
@@ -25,7 +26,7 @@ export function useTerminalWorkspaceFoundation() {
|
||||
const terminalWorktreeParkCooldownUntilRef = useRef(new Map<string, number>())
|
||||
const terminalWorktreeParkingTimersRef = useRef(new Map<string, number>())
|
||||
const worktreesById = useWorktreeMap()
|
||||
const folderWorkspaces = useAppStore((state) => state.folderWorkspaces)
|
||||
const folderWorkspaces = useAppStore((state) => selectFolderWorkspaces(state))
|
||||
const activeWorktreeId = useAppStore((state) => state.activeWorktreeId)
|
||||
const renderedActiveWorktreeId = activeWorktreeId
|
||||
const activeWorktreeDeferralHostId = useAppStore((state) =>
|
||||
|
||||
@@ -11,6 +11,11 @@ import {
|
||||
selectPaletteStatusInputs
|
||||
} from './worktree-jump-palette-status-inputs'
|
||||
import { selectWorktreePaletteCacheInputs } from '@/components/cmd-j/worktree-palette-cache-inputs'
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectFolderWorkspaces,
|
||||
selectWorktreesByRepo
|
||||
} from '@/store/workspace-record-selectors'
|
||||
|
||||
export function useWorktreeJumpPaletteStoreState({
|
||||
visible,
|
||||
@@ -29,8 +34,8 @@ export function useWorktreeJumpPaletteStoreState({
|
||||
const openSettingsTarget = useAppStore((state) => state.openSettingsTarget)
|
||||
const recordFeatureInteraction = useAppStore((state) => state.recordFeatureInteraction)
|
||||
const revealSidebarRow = useAppStore((state) => state.revealSidebarRow)
|
||||
const worktreesByRepo = useAppStore((state) => state.worktreesByRepo)
|
||||
const folderWorkspaces = useAppStore((state) => state.folderWorkspaces)
|
||||
const worktreesByRepo = useAppStore((state) => selectWorktreesByRepo(state))
|
||||
const folderWorkspaces = useAppStore((state) => selectFolderWorkspaces(state))
|
||||
const workspaces = useMemo(
|
||||
() =>
|
||||
dedupePaletteWorktrees([
|
||||
@@ -43,7 +48,7 @@ export function useWorktreeJumpPaletteStoreState({
|
||||
const projectGroups = useAppStore((state) => state.projectGroups)
|
||||
const projects = useAppStore((state) => state.projects)
|
||||
const projectHostSetups = useAppStore((state) => state.projectHostSetups)
|
||||
const detectedWorktreesByRepo = useAppStore((state) => state.detectedWorktreesByRepo)
|
||||
const detectedWorktreesByRepo = useAppStore((state) => selectDetectedWorktreesByRepo(state))
|
||||
const pendingWorktreeCreations = useAppStore((state) => state.pendingWorktreeCreations)
|
||||
const pluginCommands = usePluginCommands()
|
||||
const paletteStatusInputsActive = visible || lingering
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
createWorkspaceCleanupFacetListCache,
|
||||
createWorkspaceCleanupReviewInfoCache
|
||||
} from './workspace-cleanup-facet-row-caches'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export type WorkspaceCleanupFacetRows = {
|
||||
rows: WorkspaceCleanupFacets[]
|
||||
@@ -98,7 +99,7 @@ export function useWorkspaceCleanupFacetRows({
|
||||
}): WorkspaceCleanupFacetRows {
|
||||
const sources = useAppStore(
|
||||
useShallow((s) => ({
|
||||
worktreesByRepo: s.worktreesByRepo,
|
||||
worktreesByRepo: selectWorktreesByRepo(s),
|
||||
hostedReviewCache: s.hostedReviewCache,
|
||||
repos: s.repos,
|
||||
settings: s.settings,
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { Worktree } from '../../../../shared/worktree/types'
|
||||
import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup'
|
||||
import { isGitHubPRSuppressed } from '../../../../shared/worktree/github-pr-suppression'
|
||||
import { getWorkspaceCleanupCandidateHostId } from './workspace-cleanup-host-identity'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export type WorkspaceCleanupSortKey = 'activity' | 'name' | 'repo' | 'review' | 'git'
|
||||
export type WorkspaceCleanupSortDirection = 'asc' | 'desc'
|
||||
@@ -109,7 +110,7 @@ export function buildWorkspaceCleanupReviewLookup(
|
||||
reposById.set(repo.id, matches)
|
||||
}
|
||||
const worktreesByRepoAndId = new Map<string, Worktree[]>()
|
||||
for (const [repoId, worktrees] of Object.entries(state.worktreesByRepo)) {
|
||||
for (const [repoId, worktrees] of Object.entries(selectWorktreesByRepo(state))) {
|
||||
for (const worktree of worktrees) {
|
||||
const key = `${repoId}\0${worktree.id}`
|
||||
const matches = worktreesByRepoAndId.get(key) ?? []
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { AppState } from '@/store/types'
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectFolderWorkspaces,
|
||||
selectWorktreesByRepo
|
||||
} from '@/store/workspace-record-selectors'
|
||||
|
||||
export function pickSpaceWindowState(state: AppState) {
|
||||
return {
|
||||
@@ -8,12 +13,12 @@ export function pickSpaceWindowState(state: AppState) {
|
||||
workspaceSpaceScanning: state.workspaceSpaceScanning,
|
||||
workspaceSpaceMeasurements: state.workspaceSpaceMeasurements,
|
||||
repos: state.repos,
|
||||
worktreesByRepo: state.worktreesByRepo,
|
||||
detectedWorktreesByRepo: state.detectedWorktreesByRepo,
|
||||
worktreesByRepo: selectWorktreesByRepo(state),
|
||||
detectedWorktreesByRepo: selectDetectedWorktreesByRepo(state),
|
||||
runtimeEnvironments: state.runtimeEnvironments,
|
||||
runtimeEnvironmentCatalogHydrated: state.runtimeEnvironmentCatalogHydrated,
|
||||
removedRuntimeEnvironmentIds: state.removedRuntimeEnvironmentIds,
|
||||
folderWorkspaces: state.folderWorkspaces,
|
||||
folderWorkspaces: selectFolderWorkspaces(state),
|
||||
projectGroups: state.projectGroups,
|
||||
restoredRuntimeHostIdByWorkspaceSessionKey: state.restoredRuntimeHostIdByWorkspaceSessionKey,
|
||||
settings: state.settings,
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { WorkspaceSource as WorkspaceCreateTelemetrySource } from '../../..
|
||||
import type { LinkedWorkItemSummary } from '@/lib/new-workspace'
|
||||
import type { ComposerDecisions } from './composer-decisions'
|
||||
import type { WorkspaceCreationTargetHint } from '@/lib/project-host-workspace-target'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export type ComposerStateInput = {
|
||||
initialRepoId?: string
|
||||
@@ -104,7 +105,7 @@ export function useComposerTargetStore(options: ComposerStateInput, decisions: C
|
||||
|
||||
const newWorkspaceDraft = useAppStore((s) => s.newWorkspaceDraft)
|
||||
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const worktreesByRepo = useAppStore((s) => selectWorktreesByRepo(s))
|
||||
|
||||
const sparsePresetsByRepo = useAppStore((s) => s.sparsePresetsByRepo)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type ExecutionHostId
|
||||
} from '../../../shared/execution-host'
|
||||
import { parseWorkspaceKey } from '../../../shared/workspace-scope'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
type WatchTargetRowState = Pick<
|
||||
AppState,
|
||||
@@ -43,7 +44,7 @@ export function resolveEditorExternalWatchTargetRows(
|
||||
hostId: ExecutionHostId | null
|
||||
): EditorExternalWatchTargetRows | null {
|
||||
const worktree = pickHostRow(
|
||||
getIndexedWorktreesById(state.worktreesByRepo, worktreeId),
|
||||
getIndexedWorktreesById(selectWorktreesByRepo(state), worktreeId),
|
||||
hostId,
|
||||
(row) =>
|
||||
row.hostId ??
|
||||
@@ -55,7 +56,7 @@ export function resolveEditorExternalWatchTargetRows(
|
||||
const folderWorkspace =
|
||||
workspaceScope?.type === 'folder'
|
||||
? pickHostRow(
|
||||
state.folderWorkspaces.filter(
|
||||
selectFolderWorkspaces(state).filter(
|
||||
(workspace) => workspace.id === workspaceScope.folderWorkspaceId
|
||||
),
|
||||
hostId,
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
resolveEditorExternalWatchTargetRows
|
||||
} from './editor-external-watch-target-rows'
|
||||
import { selectWatchEligibleWorkspaceIds } from './editor-external-watch-scope'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export type EditorExternalWatchTarget = {
|
||||
worktreeId: string
|
||||
@@ -150,7 +151,7 @@ export function selectEditorExternalWatchTargets(
|
||||
const activeWorkspaceExecutionHostId = state.activeWorkspaceExecutionHostId ?? null
|
||||
if (
|
||||
cachedOpenFiles === state.openFiles &&
|
||||
cachedWorktreesByRepo === state.worktreesByRepo &&
|
||||
cachedWorktreesByRepo === selectWorktreesByRepo(state) &&
|
||||
cachedRepos === state.repos &&
|
||||
cachedActiveWorktreeId === state.activeWorktreeId &&
|
||||
cachedActiveWorkspaceExecutionHostId === activeWorkspaceExecutionHostId &&
|
||||
@@ -161,7 +162,7 @@ export function selectEditorExternalWatchTargets(
|
||||
cachedRightSidebarExplorerView === state.rightSidebarExplorerView &&
|
||||
cachedGitStatusHugeByWorktree === state.gitStatusHugeByWorktree &&
|
||||
cachedSshConnectionStates === state.sshConnectionStates &&
|
||||
cachedFolderWorkspaces === state.folderWorkspaces &&
|
||||
cachedFolderWorkspaces === selectFolderWorkspaces(state) &&
|
||||
cachedProjectGroups === state.projectGroups &&
|
||||
cachedLastVisitedAtByWorktreeId === state.lastVisitedAtByWorktreeId
|
||||
) {
|
||||
@@ -190,7 +191,7 @@ export function selectEditorExternalWatchTargets(
|
||||
}
|
||||
const activeWorktreeId = state.activeWorktreeId
|
||||
const activeWorktree = activeWorktreeId
|
||||
? findWorktreeById(state.worktreesByRepo, activeWorktreeId)
|
||||
? findWorktreeById(selectWorktreesByRepo(state), activeWorktreeId)
|
||||
: undefined
|
||||
const activeWorktreeHost = parseExecutionHostId(activeWorktree?.hostId)
|
||||
const activeRepo = activeWorktree
|
||||
@@ -238,7 +239,7 @@ export function selectEditorExternalWatchTargets(
|
||||
const targetsKey = sortedEntries.map(([key]) => key).join('|')
|
||||
const unwatchedTargetsKey = [...unwatchedTargetKeys].sort().join('|')
|
||||
cachedOpenFiles = state.openFiles
|
||||
cachedWorktreesByRepo = state.worktreesByRepo
|
||||
cachedWorktreesByRepo = selectWorktreesByRepo(state)
|
||||
cachedRepos = state.repos
|
||||
cachedActiveWorktreeId = state.activeWorktreeId
|
||||
cachedActiveWorkspaceExecutionHostId = activeWorkspaceExecutionHostId
|
||||
@@ -249,7 +250,7 @@ export function selectEditorExternalWatchTargets(
|
||||
cachedRightSidebarExplorerView = state.rightSidebarExplorerView
|
||||
cachedGitStatusHugeByWorktree = state.gitStatusHugeByWorktree
|
||||
cachedSshConnectionStates = state.sshConnectionStates
|
||||
cachedFolderWorkspaces = state.folderWorkspaces
|
||||
cachedFolderWorkspaces = selectFolderWorkspaces(state)
|
||||
cachedProjectGroups = state.projectGroups
|
||||
cachedLastVisitedAtByWorktreeId = state.lastVisitedAtByWorktreeId
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AppState } from '../../store/types'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
type AgentStatusPendingRetryState = Pick<
|
||||
AppState,
|
||||
@@ -21,7 +22,7 @@ export function shouldRetryPendingAgentStatusesAfterStoreUpdate(
|
||||
current.tabsByWorktree !== previous.tabsByWorktree ||
|
||||
current.unifiedTabsByWorktree !== previous.unifiedTabsByWorktree ||
|
||||
current.terminalLayoutsByTabId !== previous.terminalLayoutsByTabId ||
|
||||
current.worktreesByRepo !== previous.worktreesByRepo ||
|
||||
selectWorktreesByRepo(current) !== selectWorktreesByRepo(previous) ||
|
||||
current.repos !== previous.repos ||
|
||||
current.recentlyClosedAgentStatusTabIds !== previous.recentlyClosedAgentStatusTabIds ||
|
||||
current.recentlyRetiredAgentStatusPaneKeys !== previous.recentlyRetiredAgentStatusPaneKeys
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
UNFINISHED_WORKTREE_REMOVAL_ERROR,
|
||||
settleHostWorktreeRemovals
|
||||
} from '../../store/slices/worktrees/teardown/host-worktree-removal-state'
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectWorktreesByRepo
|
||||
} from '@/store/workspace-record-selectors'
|
||||
|
||||
type HostMarkedRow = Pick<Worktree, 'id' | 'hostId'>
|
||||
type AppStoreApi = Pick<typeof useAppStore, 'getState' | 'setState'>
|
||||
@@ -52,7 +56,7 @@ function showHostFailure(store: AppStoreApi, row: Worktree & { removalError: str
|
||||
export function reconcileHostWorktreeRemovals(store: AppStoreApi = useAppStore): void {
|
||||
settleHostWorktreeRemovals()
|
||||
const listed = new Map<string, Worktree>()
|
||||
for (const rows of Object.values(store.getState().worktreesByRepo)) {
|
||||
for (const rows of Object.values(selectWorktreesByRepo(store.getState()))) {
|
||||
for (const row of rows) {
|
||||
listed.set(deleteStateKey(row), row)
|
||||
}
|
||||
@@ -110,18 +114,18 @@ export function reconcileHostWorktreeRemovals(store: AppStoreApi = useAppStore):
|
||||
}
|
||||
|
||||
export function registerBackgroundWorktreeRemovalBridge(unsubs: (() => void)[]): void {
|
||||
let previousRows = useAppStore.getState().worktreesByRepo
|
||||
let previousDetected = useAppStore.getState().detectedWorktreesByRepo
|
||||
let previousRows = selectWorktreesByRepo(useAppStore.getState())
|
||||
let previousDetected = selectDetectedWorktreesByRepo(useAppStore.getState())
|
||||
unsubs.push(
|
||||
useAppStore.subscribe((state) => {
|
||||
if (
|
||||
state.worktreesByRepo === previousRows &&
|
||||
state.detectedWorktreesByRepo === previousDetected
|
||||
selectWorktreesByRepo(state) === previousRows &&
|
||||
selectDetectedWorktreesByRepo(state) === previousDetected
|
||||
) {
|
||||
return
|
||||
}
|
||||
previousRows = state.worktreesByRepo
|
||||
previousDetected = state.detectedWorktreesByRepo
|
||||
previousRows = selectWorktreesByRepo(state)
|
||||
previousDetected = selectDetectedWorktreesByRepo(state)
|
||||
reconcileHostWorktreeRemovals()
|
||||
})
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { applyWorktreeHeadIdentities } from '../worktree-head-identity-apply'
|
||||
import type { WorktreeChangeRefreshQueue } from '../worktree-change-refresh-queue'
|
||||
import { useAppStore } from '../../store'
|
||||
import { adoptFromEndpoint } from '../../store/adopt-from-endpoint'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
export function registerProjectCatalogIpcBridge(
|
||||
unsubs: (() => void)[],
|
||||
@@ -58,7 +59,7 @@ export function registerProjectCatalogIpcBridge(
|
||||
}
|
||||
const state = useAppStore.getState()
|
||||
applyWorktreeHeadIdentities(data, {
|
||||
getWorktreesForRepo: (repoId) => state.worktreesByRepo[repoId],
|
||||
getWorktreesForRepo: (repoId) => selectWorktreesByRepo(state)[repoId],
|
||||
updateWorktreeGitIdentity: state.updateWorktreeGitIdentity
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,6 +19,10 @@ import { useAppStore } from '../../store'
|
||||
import { withoutConvertedSshHostRows } from '../../store/repos/converted-ssh-host-rows'
|
||||
import { rekeyWorktreeVisitHost } from '@/lib/worktree-visit-recency'
|
||||
import { preserveConvertedSshBrowserPages } from '../../store/repos/converted-ssh-browser-pages'
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectWorktreesByRepo
|
||||
} from '@/store/workspace-record-selectors'
|
||||
|
||||
type ManagedServerStatus = SshConnectionState['managedServer']
|
||||
|
||||
@@ -217,8 +221,8 @@ function rehomeActiveWorkspaceWhenKnown(
|
||||
state.startupWorktreeRefreshCompleted && !previous.startupWorktreeRefreshCompleted
|
||||
if (
|
||||
!startupScanFinished &&
|
||||
state.worktreesByRepo === previous.worktreesByRepo &&
|
||||
state.detectedWorktreesByRepo === previous.detectedWorktreesByRepo &&
|
||||
selectWorktreesByRepo(state) === selectWorktreesByRepo(previous) &&
|
||||
selectDetectedWorktreesByRepo(state) === selectDetectedWorktreesByRepo(previous) &&
|
||||
state.activeWorktreeId === previous.activeWorktreeId &&
|
||||
state.activeWorkspaceExecutionHostId === previous.activeWorkspaceExecutionHostId
|
||||
) {
|
||||
@@ -292,8 +296,8 @@ function serverCopyOfWorkspace(
|
||||
const path = normalizeRuntimePathForComparison(source.worktreePath)
|
||||
// Why detected too: a checkout the server's visibility settings hide still exists there.
|
||||
const row = [
|
||||
...(state.worktreesByRepo[serverRepoId] ?? []),
|
||||
...(state.detectedWorktreesByRepo[serverRepoId]?.worktrees ?? [])
|
||||
...(selectWorktreesByRepo(state)[serverRepoId] ?? []),
|
||||
...(selectDetectedWorktreesByRepo(state)[serverRepoId]?.worktrees ?? [])
|
||||
].find(
|
||||
(worktree) =>
|
||||
worktree.hostId === runtimeHostId && normalizeRuntimePathForComparison(worktree.path) === path
|
||||
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
createWorktreeChangeRefreshQueue,
|
||||
type WorktreeChangeRefreshQueue
|
||||
} from '../worktree-change-refresh-queue'
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectWorktreesByRepo
|
||||
} from '@/store/workspace-record-selectors'
|
||||
|
||||
const WORKTREE_RENAME_PURGE_GRACE_MS = 20_000
|
||||
|
||||
@@ -37,13 +41,13 @@ function runtimeSortOrderFence(environmentId: string): string {
|
||||
const recentlyRenamedWorktreeIdExpiry = new Map<string, number>()
|
||||
|
||||
function getAuthoritativeDetectedWorktreeIds(state: AppState, repoId: string): Set<string> | null {
|
||||
const detected = state.detectedWorktreesByRepo[repoId]
|
||||
const detected = selectDetectedWorktreesByRepo(state)[repoId]
|
||||
return detected?.authoritative === true
|
||||
? new Set(detected.worktrees.map((worktree) => worktree.id))
|
||||
: null
|
||||
}
|
||||
function getVisibleWorktreeIdsForRepo(state: AppState, repoId: string): Set<string> {
|
||||
return new Set((state.worktreesByRepo[repoId] ?? []).map((worktree) => worktree.id))
|
||||
return new Set((selectWorktreesByRepo(state)[repoId] ?? []).map((worktree) => worktree.id))
|
||||
}
|
||||
|
||||
type WorktreeEventActivationOptions = {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { parseExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { workspaceOwnerHostId } from '../../lib/workspace-row-owner-host'
|
||||
import type { AppState } from '../../store/types'
|
||||
import { useAppStore } from '../../store'
|
||||
import { selectFolderWorkspaces } from '@/store/workspace-record-selectors'
|
||||
|
||||
/**
|
||||
* The WSL hosts an open project or terminal needs: the active workspace's host and the host of
|
||||
@@ -77,7 +78,7 @@ export function registerWslHostIpcBridge(unsubs: (() => void)[]): void {
|
||||
state.tabsByWorktree,
|
||||
state.runtimeEnvironments,
|
||||
state.repos,
|
||||
state.folderWorkspaces,
|
||||
selectFolderWorkspaces(state),
|
||||
state.projectGroups
|
||||
]
|
||||
if (next.every((value, index) => value === inputs[index])) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '@/lib/project-skill-runtime'
|
||||
import { useWindowsTerminalCapabilities } from '@/lib/windows-terminal-capabilities'
|
||||
import { useAppStore } from '@/store'
|
||||
import { selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
|
||||
type ActiveProjectSkillRuntime = {
|
||||
projectRuntime?: ProjectExecutionRuntimeResolution
|
||||
@@ -72,7 +73,7 @@ export function useActiveProjectSkillRuntime(): ActiveProjectSkillRuntime {
|
||||
projects: state.projects,
|
||||
repos: state.repos,
|
||||
settings: state.settings,
|
||||
worktreesByRepo: state.worktreesByRepo
|
||||
worktreesByRepo: selectWorktreesByRepo(state)
|
||||
}))
|
||||
)
|
||||
const currentPlatform = getCurrentPlatform()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user